Fix web push delivery handling and worker console
Some checks failed
docker-images / resolve-build-targets (push) Successful in 5s
docker-images / build-and-push (admin) (push) Successful in 30s
docker-images / submit-indexnow (push) Has been cancelled
docker-images / build-and-push (frontend) (push) Has been cancelled
docker-images / build-and-push (backend) (push) Has been cancelled

This commit is contained in:
2026-04-04 04:15:20 +08:00
parent ab18bbaf23
commit 381dc9b854
19 changed files with 1607 additions and 747 deletions

View File

@@ -114,3 +114,14 @@ export function formatWorkerProgress(
return progress.message ?? (details || null);
}
export function getWorkerProgressPercent(
job: Pick<WorkerJobRecord, "result">,
): number | null {
const progress = getWorkerProgress(job);
if (typeof progress?.percent !== "number") {
return null;
}
return Math.max(0, Math.min(100, Math.round(progress.percent)));
}

View File

@@ -10,15 +10,22 @@ import {
Star,
Tags,
Workflow,
} from 'lucide-react'
import { startTransition, useCallback, useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { toast } from 'sonner'
} from "lucide-react";
import { startTransition, useCallback, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { formatDateTime } from "@/lib/admin-format";
import {
Table,
TableBody,
@@ -26,9 +33,9 @@ import {
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { adminApi, ApiError } from '@/lib/api'
import { buildFrontendUrl } from '@/lib/frontend-url'
} from "@/components/ui/table";
import { adminApi, ApiError } from "@/lib/api";
import { buildFrontendUrl } from "@/lib/frontend-url";
import {
formatCommentScope,
formatPostStatus,
@@ -37,8 +44,12 @@ import {
formatPostVisibility,
formatReviewStatus,
formatReviewType,
} from '@/lib/admin-format'
import type { AdminAnalyticsResponse, AdminDashboardResponse, WorkerOverview } from '@/lib/types'
} from "@/lib/admin-format";
import type {
AdminAnalyticsResponse,
AdminDashboardResponse,
WorkerOverview,
} from "@/lib/types";
function StatCard({
label,
@@ -46,17 +57,21 @@ function StatCard({
note,
icon: Icon,
}: {
label: string
value: number
note: string
icon: typeof Rss
label: string;
value: number;
note: string;
icon: typeof Rss;
}) {
return (
<Card className="bg-gradient-to-br from-card via-card to-background/70">
<CardContent className="flex items-start justify-between pt-6">
<div>
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">{label}</p>
<div className="mt-3 text-3xl font-semibold tracking-tight">{value}</div>
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
{label}
</p>
<div className="mt-3 text-3xl font-semibold tracking-tight">
{value}
</div>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{note}</p>
</div>
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-primary/20 bg-primary/10 text-primary">
@@ -64,75 +79,81 @@ function StatCard({
</div>
</CardContent>
</Card>
)
);
}
function formatAiSourceLabel(value: string) {
switch (value) {
case 'chatgpt-search':
return 'ChatGPT Search'
case 'perplexity':
return 'Perplexity'
case 'copilot-bing':
return 'Copilot / Bing'
case 'gemini':
return 'Gemini'
case 'claude':
return 'Claude'
case 'google':
return 'Google'
case 'duckduckgo':
return 'DuckDuckGo'
case 'kagi':
return 'Kagi'
case 'direct':
return 'Direct'
case "chatgpt-search":
return "ChatGPT Search";
case "perplexity":
return "Perplexity";
case "copilot-bing":
return "Copilot / Bing";
case "gemini":
return "Gemini";
case "claude":
return "Claude";
case "google":
return "Google";
case "duckduckgo":
return "DuckDuckGo";
case "kagi":
return "Kagi";
case "direct":
return "Direct";
default:
return value
return value;
}
}
export function DashboardPage() {
const [data, setData] = useState<AdminDashboardResponse | null>(null)
const [workerOverview, setWorkerOverview] = useState<WorkerOverview | null>(null)
const [analytics, setAnalytics] = useState<AdminAnalyticsResponse | null>(null)
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const [data, setData] = useState<AdminDashboardResponse | null>(null);
const [workerOverview, setWorkerOverview] = useState<WorkerOverview | null>(
null,
);
const [analytics, setAnalytics] = useState<AdminAnalyticsResponse | null>(
null,
);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const loadDashboard = useCallback(async (showToast = false) => {
try {
if (showToast) {
setRefreshing(true)
setRefreshing(true);
}
const [next, nextWorkerOverview, nextAnalytics] = await Promise.all([
adminApi.dashboard(),
adminApi.getWorkersOverview(),
adminApi.analytics(),
])
]);
startTransition(() => {
setData(next)
setWorkerOverview(nextWorkerOverview)
setAnalytics(nextAnalytics)
})
setData(next);
setWorkerOverview(nextWorkerOverview);
setAnalytics(nextAnalytics);
});
if (showToast) {
toast.success('仪表盘已刷新。')
toast.success("仪表盘已刷新。");
}
} catch (error) {
if (error instanceof ApiError && error.status === 401) {
return
return;
}
toast.error(error instanceof ApiError ? error.message : '无法加载仪表盘。')
toast.error(
error instanceof ApiError ? error.message : "无法加载仪表盘。",
);
} finally {
setLoading(false)
setRefreshing(false)
setLoading(false);
setRefreshing(false);
}
}, [])
}, []);
useEffect(() => {
void loadDashboard(false)
}, [loadDashboard])
void loadDashboard(false);
}, [loadDashboard]);
if (loading || !data || !workerOverview || !analytics) {
return (
@@ -147,24 +168,24 @@ export function DashboardPage() {
<Skeleton className="h-[420px] rounded-3xl" />
</div>
</div>
)
);
}
const statCards = [
{
label: '文章总数',
label: "文章总数",
value: data.stats.total_posts,
note: `内容库中共有 ${data.stats.total_comments} 条评论`,
icon: Rss,
},
{
label: '待审核评论',
label: "待审核评论",
value: data.stats.pending_comments,
note: '等待审核处理',
note: "等待审核处理",
icon: MessageSquareWarning,
},
{
label: '发布待办',
label: "发布待办",
value:
data.stats.draft_posts +
data.stats.scheduled_posts +
@@ -174,30 +195,32 @@ export function DashboardPage() {
icon: Clock3,
},
{
label: '分类数量',
label: "分类数量",
value: data.stats.total_categories,
note: `当前共有 ${data.stats.total_tags} 个标签`,
icon: FolderTree,
},
{
label: 'AI 分块',
label: "AI 分块",
value: data.stats.ai_chunks,
note: data.stats.ai_enabled ? '知识库已启用' : 'AI 功能当前关闭',
note: data.stats.ai_enabled ? "知识库已启用" : "AI 功能当前关闭",
icon: BrainCircuit,
},
{
label: 'Worker 活动',
label: "Worker 活动",
value: workerOverview.active_jobs,
note: `失败 ${workerOverview.failed} / 运行 ${workerOverview.running}`,
icon: Workflow,
},
]
];
const aiTrafficShare =
analytics.content_overview.page_views_last_7d > 0
? (analytics.ai_discovery_page_views_last_7d / analytics.content_overview.page_views_last_7d) * 100
: 0
const topAiSource = analytics.ai_referrers_last_7d[0]
const totalAiSourceBuckets = analytics.ai_referrers_last_7d.length
? (analytics.ai_discovery_page_views_last_7d /
analytics.content_overview.page_views_last_7d) *
100
: 0;
const topAiSource = analytics.ai_referrers_last_7d[0];
const totalAiSourceBuckets = analytics.ai_referrers_last_7d.length;
return (
<div className="space-y-6">
@@ -207,14 +230,15 @@ export function DashboardPage() {
<div>
<h2 className="text-3xl font-semibold tracking-tight"></h2>
<p className="mt-2 max-w-3xl text-sm leading-7 text-muted-foreground">
AI
AI
</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button variant="outline" asChild>
<a href={buildFrontendUrl('/ask')} target="_blank" rel="noreferrer">
<a href={buildFrontendUrl("/ask")} target="_blank" rel="noreferrer">
<ArrowUpRight className="h-4 w-4" />
AI
</a>
@@ -225,7 +249,7 @@ export function DashboardPage() {
disabled={refreshing}
>
<RefreshCcw className="h-4 w-4" />
{refreshing ? '刷新中...' : '刷新'}
{refreshing ? "刷新中..." : "刷新"}
</Button>
</div>
</div>
@@ -241,9 +265,7 @@ export function DashboardPage() {
<CardHeader className="flex flex-row items-start justify-between gap-4">
<div>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
<CardDescription></CardDescription>
</div>
<Badge variant="outline">{data.recent_posts.length} </Badge>
</CardHeader>
@@ -265,9 +287,13 @@ export function DashboardPage() {
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium">{post.title}</span>
{post.pinned ? <Badge variant="success"></Badge> : null}
{post.pinned ? (
<Badge variant="success"></Badge>
) : null}
</div>
<p className="font-mono text-xs text-muted-foreground">{post.slug}</p>
<p className="font-mono text-xs text-muted-foreground">
{post.slug}
</p>
</div>
</TableCell>
<TableCell className="uppercase text-muted-foreground">
@@ -275,12 +301,18 @@ export function DashboardPage() {
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
<Badge variant="outline">{formatPostStatus(post.status)}</Badge>
<Badge variant="secondary">{formatPostVisibility(post.visibility)}</Badge>
<Badge variant="outline">
{formatPostStatus(post.status)}
</Badge>
<Badge variant="secondary">
{formatPostVisibility(post.visibility)}
</Badge>
</div>
</TableCell>
<TableCell>{post.category}</TableCell>
<TableCell className="text-muted-foreground">{post.created_at}</TableCell>
<TableCell className="text-muted-foreground">
{post.created_at}
</TableCell>
</TableRow>
))}
</TableBody>
@@ -291,19 +323,19 @@ export function DashboardPage() {
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
AI
</CardDescription>
<CardDescription> AI </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-2xl border border-border/70 bg-background/70 p-4">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium">{data.site.site_name}</p>
<p className="mt-1 text-sm text-muted-foreground">{data.site.site_url}</p>
<p className="mt-1 text-sm text-muted-foreground">
{data.site.site_url}
</p>
</div>
<Badge variant={data.site.ai_enabled ? 'success' : 'warning'}>
{data.site.ai_enabled ? 'AI 已开启' : 'AI 已关闭'}
<Badge variant={data.site.ai_enabled ? "success" : "warning"}>
{data.site.ai_enabled ? "AI 已开启" : "AI 已关闭"}
</Badge>
</div>
</div>
@@ -314,7 +346,9 @@ export function DashboardPage() {
</p>
<div className="mt-3 flex items-end gap-2">
<span className="text-3xl font-semibold">{data.stats.total_reviews}</span>
<span className="text-3xl font-semibold">
{data.stats.total_reviews}
</span>
<Star className="mb-1 h-4 w-4 text-amber-500" />
</div>
</div>
@@ -323,7 +357,9 @@ export function DashboardPage() {
</p>
<div className="mt-3 flex items-end gap-2">
<span className="text-3xl font-semibold">{data.stats.total_links}</span>
<span className="text-3xl font-semibold">
{data.stats.total_links}
</span>
<Tags className="mb-1 h-4 w-4 text-primary" />
</div>
</div>
@@ -335,25 +371,35 @@ export function DashboardPage() {
</p>
<div className="mt-3 grid gap-3 sm:grid-cols-2">
<div>
<p className="text-2xl font-semibold">{data.stats.draft_posts}</p>
<p className="text-2xl font-semibold">
{data.stats.draft_posts}
</p>
<p className="text-xs text-muted-foreground">稿</p>
</div>
<div>
<p className="text-2xl font-semibold">{data.stats.scheduled_posts}</p>
<p className="text-2xl font-semibold">
{data.stats.scheduled_posts}
</p>
<p className="text-xs text-muted-foreground"></p>
</div>
<div>
<p className="text-2xl font-semibold">{data.stats.offline_posts}</p>
<p className="text-2xl font-semibold">
{data.stats.offline_posts}
</p>
<p className="text-xs text-muted-foreground">线</p>
</div>
<div>
<p className="text-2xl font-semibold">{data.stats.expired_posts}</p>
<p className="text-2xl font-semibold">
{data.stats.expired_posts}
</p>
<p className="text-xs text-muted-foreground"></p>
</div>
</div>
<div className="mt-4 flex flex-wrap gap-2 text-xs text-muted-foreground">
<Badge variant="outline"> {data.stats.private_posts}</Badge>
<Badge variant="outline"> {data.stats.unlisted_posts}</Badge>
<Badge variant="outline">
{data.stats.unlisted_posts}
</Badge>
</div>
</div>
@@ -362,7 +408,9 @@ export function DashboardPage() {
AI
</p>
<p className="mt-3 text-sm leading-6 text-muted-foreground">
{data.site.ai_last_indexed_at ?? '站点还没有建立过索引。'}
{data.site.ai_last_indexed_at
? formatDateTime(data.site.ai_last_indexed_at)
: "站点还没有建立过索引。"}
</p>
</div>
@@ -372,9 +420,13 @@ export function DashboardPage() {
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
GEO / AI
</p>
<p className="mt-3 text-3xl font-semibold">{analytics.ai_discovery_page_views_last_7d}</p>
<p className="mt-3 text-3xl font-semibold">
{analytics.ai_discovery_page_views_last_7d}
</p>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
7 ChatGPT SearchPerplexityCopilot/BingGeminiClaude 访
7 ChatGPT
SearchPerplexityCopilot/BingGeminiClaude
访
</p>
</div>
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-primary/20 bg-primary/10 text-primary">
@@ -384,23 +436,41 @@ export function DashboardPage() {
<div className="mt-4 grid gap-3 sm:grid-cols-3">
<div className="rounded-2xl border border-border/70 bg-background/80 px-4 py-3">
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">访</div>
<div className="mt-2 text-2xl font-semibold">{Math.round(aiTrafficShare)}%</div>
<div className="mt-1 text-xs text-muted-foreground"> 7 page_view</div>
</div>
<div className="rounded-2xl border border-border/70 bg-background/80 px-4 py-3">
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground"></div>
<div className="mt-2 text-base font-semibold">
{topAiSource ? formatAiSourceLabel(topAiSource.referrer) : '暂无'}
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
访
</div>
<div className="mt-2 text-2xl font-semibold">
{Math.round(aiTrafficShare)}%
</div>
<div className="mt-1 text-xs text-muted-foreground">
{topAiSource ? `${topAiSource.count} 次访问` : '等待来源数据'}
7 page_view
</div>
</div>
<div className="rounded-2xl border border-border/70 bg-background/80 px-4 py-3">
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground"></div>
<div className="mt-2 text-2xl font-semibold">{totalAiSourceBuckets}</div>
<div className="mt-1 text-xs text-muted-foreground"> AI </div>
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
</div>
<div className="mt-2 text-base font-semibold">
{topAiSource
? formatAiSourceLabel(topAiSource.referrer)
: "暂无"}
</div>
<div className="mt-1 text-xs text-muted-foreground">
{topAiSource
? `${topAiSource.count} 次访问`
: "等待来源数据"}
</div>
</div>
<div className="rounded-2xl border border-border/70 bg-background/80 px-4 py-3">
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
</div>
<div className="mt-2 text-2xl font-semibold">
{totalAiSourceBuckets}
</div>
<div className="mt-1 text-xs text-muted-foreground">
AI
</div>
</div>
</div>
@@ -408,15 +478,24 @@ export function DashboardPage() {
<div className="mt-4 space-y-3">
{analytics.ai_referrers_last_7d.slice(0, 4).map((item) => {
const width = `${Math.max(
(item.count / Math.max(analytics.ai_discovery_page_views_last_7d, 1)) * 100,
(item.count /
Math.max(
analytics.ai_discovery_page_views_last_7d,
1,
)) *
100,
8,
)}%`
)}%`;
return (
<div key={item.referrer} className="space-y-1.5">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="font-medium">{formatAiSourceLabel(item.referrer)}</span>
<span className="text-muted-foreground">{item.count}</span>
<span className="font-medium">
{formatAiSourceLabel(item.referrer)}
</span>
<span className="text-muted-foreground">
{item.count}
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-secondary">
<div
@@ -425,11 +504,13 @@ export function DashboardPage() {
/>
</div>
</div>
)
);
})}
</div>
) : (
<p className="mt-4 text-sm text-muted-foreground"> 7 AI </p>
<p className="mt-4 text-sm text-muted-foreground">
7 AI
</p>
)}
</div>
@@ -440,12 +521,17 @@ export function DashboardPage() {
Worker
</p>
<p className="mt-3 text-sm leading-6 text-muted-foreground">
{workerOverview.queued} {workerOverview.running} {workerOverview.failed}
{workerOverview.queued}{" "}
{workerOverview.running} {workerOverview.failed}
</p>
</div>
<Button variant="outline" size="sm" asChild>
<Link
to={workerOverview.failed > 0 ? '/workers?status=failed' : '/workers'}
to={
workerOverview.failed > 0
? "/workers?status=failed"
: "/workers"
}
data-testid="dashboard-worker-open"
>
@@ -459,24 +545,36 @@ export function DashboardPage() {
data-testid="dashboard-worker-card-queued"
className="rounded-2xl border border-border/70 px-4 py-3 text-sm transition hover:border-primary/30 hover:bg-primary/5"
>
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">Queued</div>
<div className="mt-2 text-2xl font-semibold text-foreground">{workerOverview.queued}</div>
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
Queued
</div>
<div className="mt-2 text-2xl font-semibold text-foreground">
{workerOverview.queued}
</div>
</Link>
<Link
to="/workers?status=running"
data-testid="dashboard-worker-card-running"
className="rounded-2xl border border-border/70 px-4 py-3 text-sm transition hover:border-primary/30 hover:bg-primary/5"
>
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">Running</div>
<div className="mt-2 text-2xl font-semibold text-foreground">{workerOverview.running}</div>
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
Running
</div>
<div className="mt-2 text-2xl font-semibold text-foreground">
{workerOverview.running}
</div>
</Link>
<Link
to="/workers?status=failed"
data-testid="dashboard-worker-card-failed"
className="rounded-2xl border border-border/70 px-4 py-3 text-sm transition hover:border-primary/30 hover:bg-primary/5"
>
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">Failed</div>
<div className="mt-2 text-2xl font-semibold text-foreground">{workerOverview.failed}</div>
<div className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
Failed
</div>
<div className="mt-2 text-2xl font-semibold text-foreground">
{workerOverview.failed}
</div>
</Link>
</div>
@@ -489,11 +587,17 @@ export function DashboardPage() {
className="flex items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3 text-sm transition hover:border-primary/30 hover:bg-primary/5"
>
<div>
<div className="font-medium text-foreground">{item.label}</div>
<div className="mt-1 text-xs text-muted-foreground">{item.worker_name}</div>
<div className="font-medium text-foreground">
{item.label}
</div>
<div className="mt-1 text-xs text-muted-foreground">
{item.worker_name}
</div>
</div>
<div className="text-right text-xs text-muted-foreground">
<div>Q {item.queued} · R {item.running}</div>
<div>
Q {item.queued} · R {item.running}
</div>
<div>ERR {item.failed}</div>
</div>
</Link>
@@ -510,11 +614,11 @@ export function DashboardPage() {
<CardHeader className="flex flex-row items-start justify-between gap-4">
<div>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
<CardDescription></CardDescription>
</div>
<Badge variant="warning">{data.pending_comments.length} </Badge>
<Badge variant="warning">
{data.pending_comments.length}
</Badge>
</CardHeader>
<CardContent>
<Table>
@@ -543,7 +647,9 @@ export function DashboardPage() {
<TableCell className="font-mono text-xs text-muted-foreground">
{comment.post_slug}
</TableCell>
<TableCell className="text-muted-foreground">{comment.created_at}</TableCell>
<TableCell className="text-muted-foreground">
{comment.created_at}
</TableCell>
</TableRow>
))}
</TableBody>
@@ -556,11 +662,11 @@ export function DashboardPage() {
<CardHeader className="flex flex-row items-start justify-between gap-4">
<div>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
<CardDescription></CardDescription>
</div>
<Badge variant="warning">{data.pending_friend_links.length} </Badge>
<Badge variant="warning">
{data.pending_friend_links.length}
</Badge>
</CardHeader>
<CardContent className="space-y-3">
{data.pending_friend_links.map((link) => (
@@ -591,9 +697,7 @@ export function DashboardPage() {
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{data.recent_reviews.map((review) => (
@@ -604,11 +708,14 @@ export function DashboardPage() {
<div className="min-w-0">
<p className="font-medium">{review.title}</p>
<p className="mt-1 text-sm text-muted-foreground">
{formatReviewType(review.review_type)} · {formatReviewStatus(review.status)}
{formatReviewType(review.review_type)} ·{" "}
{formatReviewStatus(review.status)}
</p>
</div>
<div className="text-right">
<div className="text-lg font-semibold">{review.rating}/5</div>
<div className="text-lg font-semibold">
{review.rating}/5
</div>
<p className="text-xs uppercase tracking-[0.16em] text-muted-foreground">
{review.review_date}
</p>
@@ -620,5 +727,5 @@ export function DashboardPage() {
</div>
</div>
</div>
)
);
}

View File

@@ -26,7 +26,11 @@ import { Select } from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { adminApi, ApiError } from "@/lib/api";
import { formatWorkerProgress } from "@/lib/worker-progress";
import { formatDateTime } from "@/lib/admin-format";
import {
formatWorkerProgress,
getWorkerProgressPercent,
} from "@/lib/worker-progress";
import type {
AdminSiteSettingsResponse,
AiProviderConfig,
@@ -2119,7 +2123,9 @@ export function SiteSettingsPage() {
</p>
<p className="mt-3 text-sm leading-6 text-muted-foreground">
{form.ai_last_indexed_at ?? "索引尚未建立。"}
{form.ai_last_indexed_at
? formatDateTime(form.ai_last_indexed_at)
: "索引尚未建立。"}
</p>
{reindexJobId ? (
<p className="mt-3 text-xs leading-6 text-muted-foreground">
@@ -2133,6 +2139,18 @@ export function SiteSettingsPage() {
"任务已经开始,正在等待下一次进度更新。"}
</p>
) : null}
{reindexJobResult &&
getWorkerProgressPercent({ result: reindexJobResult }) !==
null ? (
<div className="mt-3 h-2 overflow-hidden rounded-full bg-border/70">
<div
className="h-full rounded-full bg-primary transition-[width] duration-300"
style={{
width: `${getWorkerProgressPercent({ result: reindexJobResult })}%`,
}}
/>
</div>
) : null}
</div>
</CardContent>
</Card>

File diff suppressed because it is too large Load Diff