feat(notices): bring the release notes back on every update

The release modal takes the 4.3.0 design: the release and its headline cards
on the left, the note and the support links on the right, at the design's own
size. The cards carry small drawings of the features instead of an icon.

The 4.0.0 entry is retired. In its place sits one entry, release-notes, that
every big release rewrites and keeps. It recurs per version with no upper
bound, so every user sees it once on each update, whatever they did with it
before, and a reload of the same version keeps it closed. An integration test
holds that promise through the real entry and the real dismiss route.

All 23 locales carry the new copy and drop the 4.0.0 strings.
This commit is contained in:
Maurice
2026-09-15 23:50:15 +02:00
parent 99783cc8da
commit 05ee3d50e4
34 changed files with 1562 additions and 1206 deletions
+42 -1
View File
@@ -97,7 +97,7 @@ describe('GET /api/system-notices/active', () => {
// login_count > 1 means firstLogin does not match; first_seen_version >= 3.0.0 means
// existingUserBeforeVersion('3.0.0') does not match either. Notices that gate on
// nothing but the install being self-hosted still apply, and which ones those are
// changes every release — the thank-you modal handed over to release-4-0-0 at 4.0.0.
// changes every release (the thank-you modal handed over to the release notes at 4.0.0).
// Read the set out of the registry rather than naming them, or this ages out again.
testDb.prepare('UPDATE users SET login_count = 5, first_seen_version = ? WHERE id = ?').run('3.0.0', user.id);
const alwaysOn = new Set(
@@ -226,6 +226,47 @@ describe('GET /api/system-notices/active', () => {
if (idx !== -1) SYSTEM_NOTICES.splice(idx, 1);
}
});
// The release notes carry the support links, so the promise is exact: once per
// update for every user, whatever they did with it before, and never again on a
// reload of the same version. Driven through the real registry entry and the real
// dismiss route, so neither half can drift away from the other unnoticed.
it('shows the release notes once per update, however often they were closed before', async () => {
const { user } = createUser(testDb);
testDb.prepare('UPDATE users SET login_count = 5, first_seen_version = ? WHERE id = ?').run('3.0.0', user.id);
const shows = async () => {
const res = await request(app)
.get('/api/system-notices/active')
.set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
return res.body.some((n: { id: string }) => n.id === 'release-notes');
};
const dismiss = async () => {
const res = await request(app)
.post('/api/system-notices/release-notes/dismiss')
.set('Cookie', authCookie(user.id));
expect(res.status).toBe(204);
};
expect(await shows()).toBe(true);
// Closed on this version: gone for every reload after it.
await dismiss();
expect(await shows()).toBe(false);
expect(await shows()).toBe(false);
// The next update brings it back, although it was closed before...
testDb.prepare(
'UPDATE user_notice_dismissals SET dismissed_app_version = ? WHERE user_id = ? AND notice_id = ?'
).run('4.0.0', user.id, 'release-notes');
expect(await shows()).toBe(true);
expect(await shows()).toBe(true);
// ...and closing it again holds until the one after that.
await dismiss();
expect(await shows()).toBe(false);
});
});
// ─────────────────────────────────────────────────────────────────────────────