adding version banner to check if a new version of fredy is available

This commit is contained in:
orangecoding
2025-09-20 19:37:27 +02:00
parent da8fd13973
commit f339a2e2cf
9 changed files with 118 additions and 19 deletions

View File

@@ -1 +1 @@
{"interval":"60","port":9998,"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":null,"sqlitepath":"/db"} {"interval":"60","port":9998,"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":true,"sqlitepath":"/db"}

View File

@@ -7,6 +7,7 @@ import { loginRouter } from './routes/loginRoute.js';
import { config } from '../utils.js'; import { config } from '../utils.js';
import { userRouter } from './routes/userRoute.js'; import { userRouter } from './routes/userRoute.js';
import { jobRouter } from './routes/jobRouter.js'; import { jobRouter } from './routes/jobRouter.js';
import { versionRouter } from './routes/versionRouter.js';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import restana from 'restana'; import restana from 'restana';
import files from 'serve-static'; import files from 'serve-static';
@@ -23,6 +24,7 @@ service.use(cookieSession());
service.use(staticService); service.use(staticService);
service.use('/api/admin', authInterceptor()); service.use('/api/admin', authInterceptor());
service.use('/api/jobs', authInterceptor()); service.use('/api/jobs', authInterceptor());
service.use('/api/version', authInterceptor());
// /admin can only be accessed when user is having admin permissions // /admin can only be accessed when user is having admin permissions
service.use('/api/admin', adminInterceptor()); service.use('/api/admin', adminInterceptor());
service.use('/api/jobs/notificationAdapter', notificationAdapterRouter); service.use('/api/jobs/notificationAdapter', notificationAdapterRouter);
@@ -30,6 +32,7 @@ service.use('/api/admin/generalSettings', generalSettingsRouter);
service.use('/api/jobs/provider', providerRouter); service.use('/api/jobs/provider', providerRouter);
service.use('/api/jobs/insights', analyticsRouter); service.use('/api/jobs/insights', analyticsRouter);
service.use('/api/admin/users', userRouter); service.use('/api/admin/users', userRouter);
service.use('/api/version', versionRouter);
service.use('/api/jobs', jobRouter); service.use('/api/jobs', jobRouter);
service.use('/api/login', loginRouter); service.use('/api/login', loginRouter);
//this route is unsecured intentionally as it is being queried from the login page //this route is unsecured intentionally as it is being queried from the login page

View File

@@ -0,0 +1,30 @@
import restana from 'restana';
import fetch from 'node-fetch';
import { getPackageVersion } from '../../utils.js';
const service = restana();
const versionRouter = service.newRouter();
versionRouter.get('/', async (req, res) => {
const versionPayload = await getCurrentVersionFromGithub();
res.body = versionPayload == null ? { newVersion: false } : versionPayload;
res.send();
});
async function getCurrentVersionFromGithub() {
const raw = await fetch('https://api.github.com/repos/orangecoding/fredy/releases/latest');
const data = await raw.json();
const localFredyVersion = await getPackageVersion();
if (localFredyVersion === data.tag_name) {
return null;
}
return {
newVersion: true,
version: data.tag_name,
url: data.html_url,
body: data.body,
localFredyVersion,
};
}
export { versionRouter };

View File

@@ -1,9 +1,7 @@
import { getJobs } from '../storage/jobStorage.js'; import { getJobs } from '../storage/jobStorage.js';
import { getUniqueId } from './uniqueId.js'; import { getUniqueId } from './uniqueId.js';
import { config, inDevMode } from '../../utils.js'; import { config, getPackageVersion, inDevMode } from '../../utils.js';
import os from 'os'; import os from 'os';
import { readFileSync } from 'fs';
import { packageUp } from 'package-up';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import logger from '../logger.js'; import logger from '../logger.js';
@@ -77,15 +75,3 @@ function enrichTrackingObject(trackingObject) {
version, version,
}; };
} }
async function getPackageVersion() {
try {
const packagePath = await packageUp();
const packageJson = readFileSync(packagePath, 'utf8');
const json = JSON.parse(packageJson);
return json.version;
} catch (error) {
logger.error('Error reading version from package.json', error);
}
return 'N/A';
}

View File

@@ -3,8 +3,9 @@ import { fileURLToPath } from 'node:url';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { DEFAULT_CONFIG } from './defaultConfig.js'; import { DEFAULT_CONFIG } from './defaultConfig.js';
import fs from 'fs'; import fs, { readFileSync } from 'fs';
import logger from './services/logger.js'; import logger from './services/logger.js';
import { packageUp } from 'package-up';
const RE_GT = />/g; const RE_GT = />/g;
const RE_WEBP = /\/format\/webp/gi; const RE_WEBP = /\/format\/webp/gi;
@@ -196,6 +197,22 @@ const normalizeImageUrl = (url) => {
return u; return u;
}; };
/**
* returns Fredy's version
* @returns {Promise<*|string>}
*/
async function getPackageVersion() {
try {
const packagePath = await packageUp();
const packageJson = readFileSync(packagePath, 'utf8');
const json = JSON.parse(packageJson);
return json.version;
} catch (error) {
logger.error('Error reading version from package.json', error);
}
return 'N/A';
}
await refreshConfig(); await refreshConfig();
export { isOneOf }; export { isOneOf };
@@ -206,6 +223,7 @@ export { duringWorkingHoursOrNotSet };
export { getDirName }; export { getDirName };
export { config }; export { config };
export { buildHash }; export { buildHash };
export { getPackageVersion };
export default { export default {
isOneOf, isOneOf,
nullOrEmpty, nullOrEmpty,

View File

@@ -18,11 +18,13 @@ import Jobs from './views/jobs/Jobs';
import './App.less'; import './App.less';
import TrackingModal from './components/tracking/TrackingModal.jsx'; import TrackingModal from './components/tracking/TrackingModal.jsx';
import { Banner } from '@douyinfe/semi-ui'; import { Banner } from '@douyinfe/semi-ui';
import VersionBanner from './components/version/VersionBanner.jsx';
export default function FredyApp() { export default function FredyApp() {
const actions = useActions(); const actions = useActions();
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const currentUser = useSelector((state) => state.user.currentUser); const currentUser = useSelector((state) => state.user.currentUser);
const versionUpdate = useSelector((state) => state.versionUpdate.versionUpdate);
const settings = useSelector((state) => state.generalSettings.settings); const settings = useSelector((state) => state.generalSettings.settings);
useEffect(() => { useEffect(() => {
@@ -34,6 +36,7 @@ export default function FredyApp() {
await actions.jobs.getProcessingTimes(); await actions.jobs.getProcessingTimes();
await actions.notificationAdapter.getAdapter(); await actions.notificationAdapter.getAdapter();
await actions.generalSettings.getGeneralSettings(); await actions.generalSettings.getGeneralSettings();
await actions.versionUpdate.getVersionUpdate();
} }
setLoading(false); setLoading(false);
} }
@@ -53,7 +56,6 @@ export default function FredyApp() {
<Route path="*" element={<Navigate to="/login" replace />} /> <Route path="*" element={<Navigate to="/login" replace />} />
</Routes> </Routes>
); );
return loading ? null : needsLogin() ? ( return loading ? null : needsLogin() ? (
login() login()
) : ( ) : (
@@ -62,7 +64,7 @@ export default function FredyApp() {
<Logout /> <Logout />
<Logo width={190} white /> <Logo width={190} white />
<Menu isAdmin={isAdmin()} /> <Menu isAdmin={isAdmin()} />
{versionUpdate?.newVersion && <VersionBanner />}
{settings.demoMode && ( {settings.demoMode && (
<> <>
<Banner <Banner

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Banner, Descriptions } from '@douyinfe/semi-ui';
import { useSelector } from '../../services/state/store.js';
import './VersionBanner.less';
export default function VersionBanner() {
const versionUpdate = useSelector((state) => state.versionUpdate.versionUpdate);
return (
<Banner
className="versionBanner"
type="success"
icon={null}
description={
<div>
<p>A new version of Fredy is available. Update now to take advantage of the latest features and bug fixes.</p>
<Descriptions row size="small">
<Descriptions.Item itemKey="Your Version">{versionUpdate.localFredyVersion}</Descriptions.Item>
<Descriptions.Item itemKey="Latest Version">{versionUpdate.version}</Descriptions.Item>
<Descriptions.Item itemKey="Github Release">
<a href={versionUpdate.url} target="_blank" rel="noreferrer">
{versionUpdate.url}
</a>{' '}
</Descriptions.Item>
</Descriptions>
<p>
<b>
<small>Release Notes</small>
</b>
</p>
<pre>{stripFullChangelog(versionUpdate.body)}</pre>
</div>
}
/>
);
function stripFullChangelog(text) {
if (text == null) {
return '';
}
return text.replace(/(?:\r?\n)\*\*Full Changelog\*\*[\s\S]*$/u, '');
}
}

View File

@@ -0,0 +1,3 @@
.versionBanner {
margin-bottom: 1rem;
}

View File

@@ -118,6 +118,18 @@ export const useFredyState = create(
} }
}, },
}, },
versionUpdate: {
async getVersionUpdate() {
try {
const response = await xhrGet('/api/version');
set((state) => ({
versionUpdate: { ...state.versionUpdate, versionUpdate: response.json },
}));
} catch (Exception) {
console.error('Error while trying to get resource for api/version. Error:', Exception);
}
},
},
}; };
// Initial state // Initial state
@@ -125,6 +137,7 @@ export const useFredyState = create(
notificationAdapter: [], notificationAdapter: [],
generalSettings: { settings: {} }, generalSettings: { settings: {} },
demoMode: { demoMode: false }, demoMode: { demoMode: false },
versionUpdate: {},
provider: [], provider: [],
jobs: { jobs: [], insights: {}, processingTimes: {} }, jobs: { jobs: [], insights: {}, processingTimes: {} },
user: { users: [], currentUser: null }, user: { users: [], currentUser: null },
@@ -135,6 +148,7 @@ export const useFredyState = create(
notificationAdapter: { ...effects.notificationAdapter }, notificationAdapter: { ...effects.notificationAdapter },
generalSettings: { ...effects.generalSettings }, generalSettings: { ...effects.generalSettings },
demoMode: { ...effects.demoMode }, demoMode: { ...effects.demoMode },
versionUpdate: { ...effects.versionUpdate },
provider: { ...effects.provider }, provider: { ...effects.provider },
jobs: { ...effects.jobs }, jobs: { ...effects.jobs },
user: { ...effects.user }, user: { ...effects.user },