feat(admin): update-available modal with aggregated changelog + upgrade command (#567)
Closes #567. The sidebar already had a "vX.Y.Z available" indicator (#566 made it a link to that release's page) but there was no way to read the actual changelog inline or to grab a copy-paste upgrade command. This adds the modal the issue spec'd, layered on top of the existing updateCheckService / environmentService backend infrastructure that already shipped. ## Backend - `updateCheckService.fetchAvailableVersions` now returns full release objects (tag, name, body, publishedAt, htmlUrl) instead of just version strings — body data is what the changelog modal renders. `checkForUpdates` extracts the version strings for its existing consumers; no API change visible to callers. - New `getReleasesSince(currentVersion, channel)` returns the list of releases strictly newer than current, filtered to the user's channel. Reuses the same 1-hour cache as `checkForUpdates` so the modal opening doesn't trigger an extra GitHub round-trip. - New `GET /admin/system/updates/changelog` route in `adminSystem.js`, same auth + UPDATE_CHECK_ENABLED gating as the existing /updates and /updates/instructions endpoints. - 4 unit tests (axios mocked) pin: strictly-newer filtering, channel-scoped, empty array on GitHub fetch failure, empty array when already on latest. ## Frontend - New `UpdateAvailableModal.tsx` — opens from the sidebar chip. Two sections: 1. **How to upgrade** — fetches /updates/instructions for the environment-detected copy-paste command (Docker compose / git / standalone). Copy-to-clipboard button per step. 2. **Release notes** — fetches /updates/changelog for every version between current and latest in the user's channel. Latest is auto-expanded; older releases are collapsed by default (click to expand). Each release also has a "View on GitHub" link to the canonical release page. - Renders release body markdown through the existing safe MarkdownContent component (marked + DOMPurify allowlist). - New `updateDismissal.ts` helper — single localStorage key holds the last-dismissed version. Chip stays hidden until a STRICTLY newer version appears, using the same compare semantics as the backend (stable > beta, higher beta > lower beta, semantic numeric on major.minor.patch). 9 unit tests pin the rules. - `VersionInfo.tsx` — chip is now a button that opens the modal instead of an external link (the #566 link-to-release behaviour is preserved on the modal's per-release "View on GitHub" affordance). Dismissal triggers an immediate re-render so the chip disappears without waiting for the next route change. No new dependencies — uses `marked` + `DOMPurify` that were already present in the bundle for the contract block renderer.
This commit is contained in:
@@ -7,7 +7,7 @@ const path = require('path');
|
||||
const os = require('os');
|
||||
const { formatBoolean } = require('../utils/dbCompat');
|
||||
const logger = require('../utils/logger');
|
||||
const { checkForUpdates, getCurrentChannel } = require('../services/updateCheckService');
|
||||
const { checkForUpdates, getCurrentChannel, getReleasesSince } = require('../services/updateCheckService');
|
||||
const { detectEnvironment, generateUpdateInstructions } = require('../services/environmentService');
|
||||
const {
|
||||
checkAndNotifyUpdates,
|
||||
@@ -71,6 +71,33 @@ router.get('/updates', adminAuth, requirePermission('settings.view'), async (req
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
// just the latest. Body is raw GitHub-flavoured markdown; rendering is
|
||||
// the client's job (frontend uses `marked`).
|
||||
router.get('/updates/changelog', adminAuth, requirePermission('settings.view'), async (req, res) => {
|
||||
try {
|
||||
const updateCheckEnabled = process.env.UPDATE_CHECK_ENABLED !== 'false';
|
||||
if (!updateCheckEnabled) {
|
||||
return res.json({ enabled: false, releases: [] });
|
||||
}
|
||||
|
||||
const updateInfo = await checkForUpdates();
|
||||
const releases = await getReleasesSince(updateInfo.current, updateInfo.channel);
|
||||
|
||||
res.json({
|
||||
enabled: true,
|
||||
current: updateInfo.current,
|
||||
channel: updateInfo.channel,
|
||||
releases,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching update changelog:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch changelog' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get update instructions for current environment
|
||||
router.get('/updates/instructions', adminAuth, requirePermission('settings.view'), async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user