diff --git a/server/routes/system.ts b/server/routes/system.ts index 4605121f..0522f286 100644 --- a/server/routes/system.ts +++ b/server/routes/system.ts @@ -35,8 +35,9 @@ const app = new Hono() const info = await buildCloudInstanceInfo(db, { url: origin, runtime: runtimeInfo(platform) }) return c.json(info) }) - .get('/changelog', requireAdmin, async (c) => { - const { latestVersion, markdown } = await fetchChangelog() + .get('/changelog', requireAdmin, zValidator('query', z.object({ refresh: z.string().optional() })), async (c) => { + const force = c.req.valid('query').refresh === 'true' + const { latestVersion, markdown } = await fetchChangelog(Date.now(), { force }) const currentVersion = getAppVersion() const updateAvailable = latestVersion ? compareSemver(latestVersion, currentVersion) > 0 : false return c.json({ currentVersion, latestVersion, updateAvailable, markdown }) diff --git a/server/services/changelog.ts b/server/services/changelog.ts index b37f64b1..9ad60a96 100644 --- a/server/services/changelog.ts +++ b/server/services/changelog.ts @@ -39,8 +39,11 @@ export function resetChangelogCache(): void { cache = null } -export async function fetchChangelog(now: number = Date.now()): Promise { - if (cache && now - cache.at < TTL_MS) { +export async function fetchChangelog( + now: number = Date.now(), + opts: { force?: boolean } = {}, +): Promise { + if (!opts.force && cache && now - cache.at < TTL_MS) { return cache.value } const [latestVersion, markdown] = await Promise.all([fetchLatestReleaseVersion(), fetchChangelogMarkdown()]) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 279d88fa..32b6cb2d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1342,6 +1342,7 @@ "admin.about.updateAvailable": "Update available", "admin.about.upToDate": "Up to date", "admin.about.commit": "Commit", + "admin.about.refresh": "Refresh changelog", "admin.about.commitTooltip": "View this commit on GitHub", "admin.about.changelogTitle": "Changelog", "admin.about.changelogSubtitle": "What's new in each release.", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 063795ab..1f2a7bbb 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1342,6 +1342,7 @@ "admin.about.updateAvailable": "有新版本", "admin.about.upToDate": "已是最新", "admin.about.commit": "提交", + "admin.about.refresh": "刷新更新日志", "admin.about.commitTooltip": "在 GitHub 上查看此提交", "admin.about.changelogTitle": "更新日志", "admin.about.changelogSubtitle": "了解每个版本的新增功能。", diff --git a/src/lib/api.test.ts b/src/lib/api.test.ts index 6569e537..a2352ebb 100644 --- a/src/lib/api.test.ts +++ b/src/lib/api.test.ts @@ -2955,6 +2955,16 @@ describe('api', () => { expect(result).toEqual(changelog) }) + it('appends refresh=true to bypass the server cache', async () => { + vi.mocked(fetch).mockResolvedValueOnce(makeResponse(changelog)) + + await getChangelog({ refresh: true }) + + const [url] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] + expect(url).toContain('/api/system/changelog') + expect(url).toContain('refresh=true') + }) + it('throws ApiError on failure', async () => { vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ error: 'Forbidden' }, false, 403)) diff --git a/src/lib/api.ts b/src/lib/api.ts index cbe5767d..73789255 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1029,8 +1029,8 @@ export function getInstanceInfo() { return unwrap(system.instance.$get()) } -export function getChangelog() { - return unwrap(system.changelog.$get()) +export function getChangelog(opts?: { refresh?: boolean }) { + return unwrap(system.changelog.$get({ query: opts?.refresh ? { refresh: 'true' } : {} })) } export function connectCloud() { diff --git a/src/routes/_authenticated/admin/about.tsx b/src/routes/_authenticated/admin/about.tsx index 02c22b4f..efae564d 100644 --- a/src/routes/_authenticated/admin/about.tsx +++ b/src/routes/_authenticated/admin/about.tsx @@ -1,7 +1,7 @@ import { githubCommitUrl, ZPAN_CLOUD_URL_DEFAULT, ZPAN_GITHUB_URL } from '@shared/constants' -import { useQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' -import { ArrowUpCircle, BadgeCheck, ExternalLink, Github, Server, Sparkles, Star } from 'lucide-react' +import { ArrowUpCircle, BadgeCheck, ExternalLink, Github, RefreshCw, Server, Sparkles, Star } from 'lucide-react' import { type ReactNode, useState } from 'react' import { useTranslation } from 'react-i18next' import { AdminPageHeader } from '@/components/admin/admin-page-header' @@ -31,6 +31,8 @@ function AboutPage() { const { t } = useTranslation() const { bound, active, edition, licenseId, cloudDashboardUrl } = useEntitlement() const [changelogOpen, setChangelogOpen] = useState(false) + const [refreshing, setRefreshing] = useState(false) + const queryClient = useQueryClient() const { data: instance, isLoading } = useQuery({ queryKey: ['system', 'instance'], queryFn: getInstanceInfo, @@ -38,10 +40,22 @@ function AboutPage() { }) const { data: changelog, isError: changelogError } = useQuery({ queryKey: ['system', 'changelog'], - queryFn: getChangelog, + queryFn: () => getChangelog(), staleTime: 60 * 60 * 1000, }) + // Bypass the server-side cache (?refresh=true) and replace the cached data so + // the drawer shows the freshly fetched changelog without a full refetch. + async function refreshChangelog() { + setRefreshing(true) + try { + const fresh = await getChangelog({ refresh: true }) + queryClient.setQueryData(['system', 'changelog'], fresh) + } finally { + setRefreshing(false) + } + } + if (isLoading || !instance) return null const key = editionKey(bound, edition) @@ -199,6 +213,17 @@ function AboutPage() { {t('admin.about.changelogTitle')} {t('admin.about.changelogSubtitle')} +
{changelogError ? (

{t('admin.about.changelogError')}