Skip to content
Merged
19 changes: 13 additions & 6 deletions web/app/[locale]/admin/admin-client.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState } from "react";
import { draftStorageKey, type AgentDraft } from "@/lib/community-agent";
import { draftStorageKey, reviewedBodyHash, type AgentDraft } from "@/lib/community-agent";

interface Props {
drafts: AgentDraft[];
Expand All @@ -17,13 +17,17 @@ export function AdminClient({ drafts, posted, isZh, typeLabels }: Props) {
const [editBody, setEditBody] = useState("");
const [loading, setLoading] = useState<string | null>(null);

const handleAction = async (draftKey: string, action: "post" | "discard", editedBody?: string) => {
const handleAction = async (draft: AgentDraft, action: "post" | "discard", editedBody?: string) => {
const draftKey = draftStorageKey(draft);
setLoading(draftKey);
try {
// Binds the action to the text shown here; the route refuses it if the
// stored draft was regenerated since this page loaded.
const reviewedSha256 = await reviewedBodyHash(isZh ? draft.bodyZh : draft.bodyEn);
const res = await fetch("/api/admin/post", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action, draftKey, editedBody, lang: isZh ? "zh" : "en" }),
body: JSON.stringify({ action, draftKey, editedBody, lang: isZh ? "zh" : "en", reviewedSha256 }),
});
const data = await res.json();
if (data.ok) {
Expand All @@ -37,6 +41,9 @@ export function AdminClient({ drafts, posted, isZh, typeLabels }: Props) {
}
}
setEditing(null);
// The post went through but something after it did not (draft state
// not saved, or the digest was not published on /digest).
if (typeof data.warning === "string") alert(data.warning);
} else {
alert(`Error: ${data.error}`);
}
Expand Down Expand Up @@ -90,7 +97,7 @@ export function AdminClient({ drafts, posted, isZh, typeLabels }: Props) {
/>
<div className="flex gap-2">
<button
onClick={() => handleAction(key, "post", editBody)}
onClick={() => handleAction(draft, "post", editBody)}
disabled={loading === key}
className="btn btn-primary disabled:opacity-50"
>
Expand All @@ -111,7 +118,7 @@ export function AdminClient({ drafts, posted, isZh, typeLabels }: Props) {
</div>
<div className="flex gap-2 flex-wrap">
<button
onClick={() => handleAction(key, "post")}
onClick={() => handleAction(draft, "post")}
disabled={loading === key}
className="btn btn-primary disabled:opacity-50"
>
Expand All @@ -124,7 +131,7 @@ export function AdminClient({ drafts, posted, isZh, typeLabels }: Props) {
{isZh ? "编辑后发布" : "Edit & post"}
</button>
<button
onClick={() => handleAction(key, "discard")}
onClick={() => handleAction(draft, "discard")}
disabled={loading === key}
className="btn btn-danger"
>
Expand Down
44 changes: 20 additions & 24 deletions web/app/[locale]/digest/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/surface-state";
import { getDigest, pickTextLocale } from "@/lib/i18n/dictionaries";
import {
DIGEST_RECORD_PREFIX,
isPublishedDigest,
type WeeklyDigestRecord as WeeklyDigest,
} from "@/lib/community-agent";
import { getDigest } from "@/lib/i18n/dictionaries";
import { getEnv } from "@/lib/kv";
import { buildPageMetadata } from "@/lib/page-meta";

// Define the exact structure of the Digest data to fix all the 'any' type errors
interface DigestSection {
heading: string;
items: string[];
}

interface WeeklyDigest {
weekId: string;
titleEn: string;
titleZh: string;
summaryEn: string;
summaryZh: string;
sections: DigestSection[];
generatedAt: string;
}
type DigestSection = WeeklyDigest["sections"][number];

export const revalidate = 3600; // Cache page updates hourly

Expand Down Expand Up @@ -47,7 +38,7 @@ export default async function DigestArchivePage({ params }: { params: Promise<{
if (kv) {
try {
// Fetch all weekly digest keys generated by the agent tasks
const { keys } = await kv.list({ prefix: "digest:weekly-" });
const { keys } = await kv.list({ prefix: DIGEST_RECORD_PREFIX });

if (keys && keys.length > 0) {
const digestsRaw = await Promise.all(
Expand All @@ -58,12 +49,14 @@ export default async function DigestArchivePage({ params }: { params: Promise<{
);

// Parse each entry independently so a single malformed record can't
// blank the entire archive — skip only the bad ones.
// blank the entire archive — skip only the bad ones. The cron stages
// records unapproved; only maintainer-approved ones are public.
digests = digestsRaw
.filter((item: string | null): item is string => Boolean(item))
.flatMap((item: string) => {
try {
return [JSON.parse(item) as WeeklyDigest];
const parsed: unknown = JSON.parse(item);
return isPublishedDigest(parsed) ? [parsed] : [];
} catch (e) {
console.error("Skipping malformed digest entry:", e);
return [];
Expand All @@ -78,9 +71,9 @@ export default async function DigestArchivePage({ params }: { params: Promise<{
}
}

// The records are bilingual; show the reader's language (English for
// every locale without a Chinese record).
const zh = pickTextLocale(locale) === "zh";
// A maintainer reviews a digest in one language (the admin locale), and
// only that language is published, so each digest renders in the language
// it was reviewed in whatever the reader's locale.

if (digests.length === 0) {
return (
Expand All @@ -98,7 +91,9 @@ export default async function DigestArchivePage({ params }: { params: Promise<{
<PageHeader title={t.title} lede={t.lead} pose="read" />
<div className="page-body">
<div className="page-body-narrow digest-list">
{digests.map((digest: WeeklyDigest) => (
{digests.map((digest: WeeklyDigest) => {
const zh = digest.approvedLang === "zh";
return (
<article key={digest.weekId} className="changelog-release" lang={zh ? "zh" : "en"}>
<header className="digest-head">
<span className="pill tabular">{digest.weekId}</span>
Expand All @@ -118,7 +113,8 @@ export default async function DigestArchivePage({ params }: { params: Promise<{
))}
</div>
</article>
))}
);
})}
</div>
</div>
</>
Expand Down
Loading
Loading