返回 CodeWhale
page.tsx
根目录 / web / app / [locale] / admin / page.tsx
1 import type { Metadata } from "next";
2 import { cookies } from "next/headers";
3 import { ConnectionBanner } from "@/components/connection-banner";
4 import { EmptyState, ErrorState } from "@/components/surface-state";
5 import { WhalePose } from "@/components/whale-pose";
6 import { getAgentEnv, listDrafts, validateSession, type AgentDraft } from "@/lib/community-agent";
7 import { AdminClient } from "./admin-client";
8
9 export const dynamic = "force-dynamic";
10
11 // Maintainer-only surface: keep it out of search indexes (robots.ts also
12 // disallows /*/admin).
13 export const metadata: Metadata = {
14 robots: { index: false, follow: false },
15 };
16
17 const TYPE_LABELS: Record<string, { en: string; zh: string }> = {
18 triage: { en: "Issue Triage", zh: "议题分类" },
19 "pr-review": { en: "PR Review", zh: "PR 审阅" },
20 stale: { en: "Stale Nudge", zh: "过期提醒" },
21 dupes: { en: "Duplicate", zh: "重复检测" },
22 digest: { en: "Weekly Digest", zh: "每周摘要" },
23 linkcheck: { en: "Broken Link", zh: "失效链接" },
24 "semantic-drift": { en: "Content Drift", zh: "内容漂移" },
25 };
26
27 function LoginForm({ locale, error }: { locale: string; error: boolean }) {
28 const isZh = locale === "zh";
29 return (
30 <div className="account-entry">
31 <ConnectionBanner locale={locale} />
32 <WhalePose pose="listen" className="account-entry-pose" />
33 <h1 className="page-title">
34 {isZh ? "维护者登录" : "Maintainer login"}
35 </h1>
36 <form method="POST" action={`/api/admin/login?locale=${locale}`} autoComplete="off" className="account-form">
37 <input type="hidden" name="locale" value={locale} />
38 <label className="field">
39 <span className="field-label">{isZh ? "令牌" : "Token"}</span>
40 <input
41 type="password"
42 name="token"
43 required
44 autoFocus
45 autoComplete="off"
46 spellCheck={false}
47 aria-invalid={error || undefined}
48 aria-describedby={error ? "admin-token-error" : undefined}
49 className="field-input font-mono"
50 />
51 </label>
52 <button
53 type="submit"
54 className="btn btn-primary btn-lg"
55 >
56 {isZh ? "登录" : "Sign in"}
57 </button>
58 {error && (
59 <p id="admin-token-error" className="field-hint field-hint-error" role="alert">
60 {isZh ? "令牌错误。" : "Invalid token."}
61 </p>
62 )}
63 </form>
64 </div>
65 );
66 }
67
68 export default async function AdminPage({
69 params,
70 searchParams,
71 }: {
72 params: Promise<{ locale: string }>;
73 searchParams: Promise<{ err?: string }>;
74 }) {
75 const { locale } = await params;
76 const { err } = await searchParams;
77 const isZh = locale === "zh";
78
79 const env = await getAgentEnv();
80
81 if (!env.MAINTAINER_TOKEN) {
82 return (
83 <div className="route-state">
84 <ConnectionBanner locale={locale} />
85 <div className="mt-6" />
86 <ErrorState
87 locale={locale}
88 title={isZh ? "未配置" : "Not configured"}
89 titleAs="h1"
90 body={
91 isZh
92 ? "MAINTAINER_TOKEN 未设置。请在部署前配置此环境变量。"
93 : "MAINTAINER_TOKEN is not set. Configure this secret before deployment."
94 }
95 />
96 </div>
97 );
98 }
99
100 const cookieStore = await cookies();
101 const sid = cookieStore.get("mt_sid")?.value;
102 const authed = await validateSession(env.CURATED_KV, sid);
103
104 if (!authed) {
105 return <LoginForm locale={locale} error={err === "1"} />;
106 }
107
108 let drafts: AgentDraft[] = [];
109 try {
110 drafts = await listDrafts(env.CURATED_KV);
111 } catch (e) {
112 console.error("failed to list drafts", e);
113 }
114
115 const pending = drafts.filter((d) => !d.posted);
116 const posted = drafts.filter((d) => d.posted);
117
118 return (
119 <section className="page-body admin-page">
120 {/* The signed-in shell: typed offline / reconnect state with a real
121 server probe, so a paused action is never mistaken for a posted one. */}
122 <ConnectionBanner locale={locale} />
123 <div className="admin-head">
124 <div>
125 <h1 className="page-title">
126 {isZh ? "社区助理草稿" : "Community Assistant Drafts"}
127 </h1>
128 <p className="page-meta tabular">
129 {pending.length} pending · {posted.length} posted
130 </p>
131 </div>
132 <form method="POST" action={`/api/admin/logout?locale=${locale}`}>
133 <button
134 type="submit"
135 className="btn btn-ghost btn-sm"
136 >
137 {isZh ? "退出" : "Sign out"}
138 </button>
139 </form>
140 </div>
141
142 {pending.length === 0 && posted.length === 0 && (
143 <EmptyState
144 locale={locale}
145 title={isZh ? "暂无草稿" : "No drafts yet"}
146 body={
147 isZh
148 ? "草稿将在 cron 运行后出现。可在 wrangler.jsonc 中配置触发时间。"
149 : "Drafts will appear here after cron runs. Configure triggers in wrangler.jsonc."
150 }
151 />
152 )}
153
154 <AdminClient
155 drafts={pending}
156 posted={posted}
157 isZh={isZh}
158 typeLabels={TYPE_LABELS}
159 />
160 </section>
161 );
162 }
163
163 lines Plain Text