mirror of
https://github.com/saltbo/zpan.git
synced 2026-10-29 18:05:37 +08:00
feat(about): add changelog refresh button that bypasses the cache
The changelog is cached server-side (1h) and by React Query, so stale content lingered. Add a refresh icon in the drawer header that calls the endpoint with ?refresh=true to skip the server cache and replaces the cached query data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b9d4429286
commit
dc936e5eff
@@ -35,8 +35,9 @@ const app = new Hono<Env>()
|
||||
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 })
|
||||
|
||||
@@ -39,8 +39,11 @@ export function resetChangelogCache(): void {
|
||||
cache = null
|
||||
}
|
||||
|
||||
export async function fetchChangelog(now: number = Date.now()): Promise<ChangelogSource> {
|
||||
if (cache && now - cache.at < TTL_MS) {
|
||||
export async function fetchChangelog(
|
||||
now: number = Date.now(),
|
||||
opts: { force?: boolean } = {},
|
||||
): Promise<ChangelogSource> {
|
||||
if (!opts.force && cache && now - cache.at < TTL_MS) {
|
||||
return cache.value
|
||||
}
|
||||
const [latestVersion, markdown] = await Promise.all([fetchLatestReleaseVersion(), fetchChangelogMarkdown()])
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "了解每个版本的新增功能。",
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
+2
-2
@@ -1029,8 +1029,8 @@ export function getInstanceInfo() {
|
||||
return unwrap<InstanceInfo>(system.instance.$get())
|
||||
}
|
||||
|
||||
export function getChangelog() {
|
||||
return unwrap<ChangelogInfo>(system.changelog.$get())
|
||||
export function getChangelog(opts?: { refresh?: boolean }) {
|
||||
return unwrap<ChangelogInfo>(system.changelog.$get({ query: opts?.refresh ? { refresh: 'true' } : {} }))
|
||||
}
|
||||
|
||||
export function connectCloud() {
|
||||
|
||||
@@ -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() {
|
||||
<SheetTitle>{t('admin.about.changelogTitle')}</SheetTitle>
|
||||
<SheetDescription>{t('admin.about.changelogSubtitle')}</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={refreshChangelog}
|
||||
disabled={refreshing}
|
||||
title={t('admin.about.refresh')}
|
||||
className="absolute top-4 right-12 h-7 w-7 text-muted-foreground"
|
||||
>
|
||||
<RefreshCw className={refreshing ? 'h-4 w-4 animate-spin' : 'h-4 w-4'} />
|
||||
<span className="sr-only">{t('admin.about.refresh')}</span>
|
||||
</Button>
|
||||
<div className="flex-1 overflow-y-auto px-4 pb-6">
|
||||
{changelogError ? (
|
||||
<p className="text-sm text-muted-foreground">{t('admin.about.changelogError')}</p>
|
||||
|
||||
Reference in New Issue
Block a user