feat(updates): "What's New" highlights after update + pre-update teaser
Surfaces release highlights to admins, sourced from the GitHub release notes (no AI at runtime). Bullets are written once per release in CI via GitHub Models (see docs/ci/whatsnew-highlights.yml) into a <!-- whatsnew --> block; the app reads that block and falls back to the changelog's "### Features" for releases without it — so it works against today's releases immediately. - backend utils/whatsNew.parseWhatsNew(body): curated block else Features section, strips scope/PR-links, de-dups, caps at 8 (tested). - GET /admin/system/updates/whatsnew: highlights for every version moved through since the per-instance marker (whatsnew_last_seen_version); fresh installs self-anchor silently. Best-effort, never errors. - POST /admin/system/updates/whatsnew/seen: advance the marker (per-instance). - /admin/system/updates also returns latestHighlights for the teaser. - Frontend: WhatsNewBanner (green bar -> modal with "Full changelog" link) on the dashboard via adminService; UpdateNotification shows a "New features include:" teaser. i18n de/en. No migration (uses app_settings).
This commit is contained in:
@@ -7,7 +7,9 @@ const path = require('path');
|
||||
const os = require('os');
|
||||
const { formatBoolean } = require('../utils/dbCompat');
|
||||
const logger = require('../utils/logger');
|
||||
const { checkForUpdates, getCurrentChannel, getReleasesSince } = require('../services/updateCheckService');
|
||||
const { checkForUpdates, getCurrentChannel, getCurrentVersion, getReleasesSince, compareVersions } = require('../services/updateCheckService');
|
||||
const { getAppSetting, upsertAppSetting } = require('../utils/appSettings');
|
||||
const { parseWhatsNew } = require('../utils/whatsNew');
|
||||
const { detectEnvironment, generateUpdateInstructions } = require('../services/environmentService');
|
||||
const {
|
||||
checkAndNotifyUpdates,
|
||||
@@ -61,9 +63,20 @@ router.get('/updates', adminAuth, requirePermission('settings.view'), async (req
|
||||
const forceRefresh = req.query.refresh === 'true';
|
||||
const updateInfo = await checkForUpdates(forceRefresh);
|
||||
|
||||
// Pre-update teaser: the target version's top highlights, so the
|
||||
// "Update Available" banner can show "New features include …".
|
||||
let latestHighlights = [];
|
||||
if (updateInfo.updateAvailable) {
|
||||
try {
|
||||
const newer = await getReleasesSince(updateInfo.current, updateInfo.channel);
|
||||
if (newer[0]) latestHighlights = parseWhatsNew(newer[0].body);
|
||||
} catch (_) { /* teaser is best-effort */ }
|
||||
}
|
||||
|
||||
res.json({
|
||||
enabled: true,
|
||||
...updateInfo
|
||||
...updateInfo,
|
||||
latestHighlights
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error checking for updates:', error);
|
||||
@@ -71,6 +84,69 @@ router.get('/updates', adminAuth, requirePermission('settings.view'), async (req
|
||||
}
|
||||
});
|
||||
|
||||
// What's New — after-update highlights. Returns the curated bullets for
|
||||
// every release the instance moved THROUGH since it last acknowledged one
|
||||
// (lastSeen < version <= running). Seen-tracking is per-INSTANCE: the first
|
||||
// admin to dismiss clears it for everyone (a single app_settings row). A
|
||||
// brand-new install initialises the marker silently so it never pops
|
||||
// "what's new" with nothing to compare against. Best-effort: any failure
|
||||
// (GitHub unreachable, etc.) returns hasNews:false, never errors.
|
||||
router.get('/updates/whatsnew', adminAuth, requirePermission('settings.view'), async (req, res) => {
|
||||
try {
|
||||
if (process.env.UPDATE_CHECK_ENABLED === 'false') {
|
||||
return res.json({ enabled: false, hasNews: false });
|
||||
}
|
||||
const running = await getCurrentVersion();
|
||||
const channel = getCurrentChannel(running);
|
||||
const lastSeen = await getAppSetting('whatsnew_last_seen_version', null);
|
||||
|
||||
if (!lastSeen) {
|
||||
await upsertAppSetting('whatsnew_last_seen_version', JSON.stringify(running), 'system');
|
||||
return res.json({ enabled: true, hasNews: false, running });
|
||||
}
|
||||
if (compareVersions(running, lastSeen) <= 0) {
|
||||
return res.json({ enabled: true, hasNews: false, running });
|
||||
}
|
||||
|
||||
// Releases in (lastSeen, running], newest-first, with their highlights.
|
||||
const releases = (await getReleasesSince(lastSeen, channel))
|
||||
.filter((r) => compareVersions(r.version, running) <= 0);
|
||||
const versions = releases
|
||||
.map((r) => ({
|
||||
version: r.version,
|
||||
name: r.name,
|
||||
publishedAt: r.publishedAt,
|
||||
htmlUrl: r.htmlUrl,
|
||||
bullets: parseWhatsNew(r.body),
|
||||
}))
|
||||
.filter((v) => v.bullets.length > 0);
|
||||
|
||||
return res.json({
|
||||
enabled: true,
|
||||
hasNews: versions.length > 0,
|
||||
fromVersion: lastSeen,
|
||||
toVersion: running,
|
||||
versions,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error building what\'s-new:', error);
|
||||
res.json({ enabled: true, hasNews: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Acknowledge the What's New — advance the per-instance marker to the
|
||||
// running version so it stops showing for every admin.
|
||||
router.post('/updates/whatsnew/seen', adminAuth, requirePermission('settings.view'), async (req, res) => {
|
||||
try {
|
||||
const running = await getCurrentVersion();
|
||||
await upsertAppSetting('whatsnew_last_seen_version', JSON.stringify(running), 'system');
|
||||
res.json({ ok: true, lastSeen: running });
|
||||
} catch (error) {
|
||||
logger.error('Error marking what\'s-new seen:', error);
|
||||
res.status(500).json({ error: 'Failed to update marker' });
|
||||
}
|
||||
});
|
||||
|
||||
// Aggregated changelog — every release between current and latest in
|
||||
// the user's channel. Powers the update-available modal (#567) so the
|
||||
// admin can read release notes for ALL versions they're behind on, not
|
||||
|
||||
Reference in New Issue
Block a user