From fb4a144fcfa97d3d7dc147c3a145968ef4fbbfc0 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 7 Jun 2026 00:32:11 +0800 Subject: [PATCH] Use static terminal update announcements --- .../api/system/update-announcement/route.ts | 30 ----- .../UpdateAnnouncementButton.tsx | 75 ++++++----- .../__tests__/updateAnnouncement.test.ts | 49 ++++---- .../ops/config/ConfigPageClient.tsx | 117 ++++-------------- frontend/middleware.ts | 1 - src/database/db_manager.py | 95 -------------- tests/test_update_announcement.py | 63 ---------- web/routers/system.py | 6 - web/services/ops_api.py | 65 +--------- web/services/system_api.py | 52 -------- 10 files changed, 91 insertions(+), 462 deletions(-) delete mode 100644 frontend/app/api/system/update-announcement/route.ts delete mode 100644 tests/test_update_announcement.py diff --git a/frontend/app/api/system/update-announcement/route.ts b/frontend/app/api/system/update-announcement/route.ts deleted file mode 100644 index 71d64015..00000000 --- a/frontend/app/api/system/update-announcement/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NextResponse } from "next/server"; -import { buildProxyExceptionResponse } from "@/lib/api-proxy"; - -const API_BASE = process.env.POLYWEATHER_API_BASE_URL; -const BACKEND = API_BASE ? `${API_BASE}/api/system/update-announcement` : ""; - -export async function GET() { - if (!API_BASE) { - return NextResponse.json( - { error: "POLYWEATHER_API_BASE_URL is not configured" }, - { status: 500 }, - ); - } - - try { - const res = await fetch(BACKEND, { cache: "no-store" }); - const raw = await res.text(); - return new NextResponse(raw, { - status: res.status, - headers: { - "Cache-Control": "no-store", - "Content-Type": "application/json", - }, - }); - } catch (error) { - return buildProxyExceptionResponse(error, { - publicMessage: "Failed to fetch update announcement", - }); - } -} diff --git a/frontend/components/dashboard/scan-terminal/UpdateAnnouncementButton.tsx b/frontend/components/dashboard/scan-terminal/UpdateAnnouncementButton.tsx index d7586dcb..c5693ca0 100644 --- a/frontend/components/dashboard/scan-terminal/UpdateAnnouncementButton.tsx +++ b/frontend/components/dashboard/scan-terminal/UpdateAnnouncementButton.tsx @@ -4,22 +4,52 @@ import { Megaphone, X } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; type AnnouncementText = { - title?: string; - body?: string; + title: string; + body: string; }; -type UpdateAnnouncementPayload = { - enabled?: boolean; - zh?: AnnouncementText; - en?: AnnouncementText; - updated_at?: string; +type StaticUpdateAnnouncement = { + id: string; + publishedAt: string; + expiresAt: string; + zh: AnnouncementText; + en: AnnouncementText; }; type UpdateAnnouncementButtonProps = { isEn: boolean; }; -function pickAnnouncementText(payload: UpdateAnnouncementPayload, isEn: boolean) { +const STATIC_UPDATE_ANNOUNCEMENTS: StaticUpdateAnnouncement[] = [ + { + id: "feedback-status-2026-06", + publishedAt: "2026-06-07T00:00:00+08:00", + expiresAt: "2026-07-15T00:00:00+08:00", + zh: { + title: "更新公告:终端新增公告与反馈状态", + body: + "PolyWeather 天气决策台新增“更新公告”入口,后续产品更新、数据源调整和重要说明会在这里同步。\n\n" + + "用户反馈系统也已升级:提交反馈时会自动附带相关图表上下文,用户可以在终端右上角通知入口和账户页查看自己反馈的处理状态,包括已收到、已确认、处理中、已解决和已关闭。\n\n" + + "我们也在考虑对真实、可复现、有建设性价值的反馈加入积分或 Pro 天数激励。", + }, + en: { + title: "Update: announcements and feedback status are now live", + body: + "PolyWeather Terminal now has an update announcement entry. Future product updates, data-source changes, and important notes will be shared here.\n\n" + + "The feedback system has also been upgraded. When users submit feedback, PolyWeather automatically attaches the relevant chart context. Users can now track their own feedback status from the terminal notification entry and the account page: received, confirmed, in progress, resolved, or closed.\n\n" + + "We are also considering small rewards such as points or Pro days for real, reproducible, and constructive feedback.", + }, + }, +]; + +function isActiveAnnouncement(item: StaticUpdateAnnouncement, now = Date.now()) { + const expiresAt = Date.parse(item.expiresAt); + if (!Number.isFinite(expiresAt) || expiresAt <= now) return false; + const publishedAt = Date.parse(item.publishedAt); + return !Number.isFinite(publishedAt) || publishedAt <= now; +} + +function pickAnnouncementText(payload: StaticUpdateAnnouncement, isEn: boolean) { const primary = isEn ? payload.en : payload.zh; const fallback = isEn ? payload.zh : payload.en; return { @@ -42,30 +72,9 @@ function formatUpdatedAt(value: string | undefined, isEn: boolean) { } export function UpdateAnnouncementButton({ isEn }: UpdateAnnouncementButtonProps) { - const [announcement, setAnnouncement] = useState(null); const [open, setOpen] = useState(false); const shellRef = useRef(null); - useEffect(() => { - let cancelled = false; - async function loadAnnouncement() { - try { - const res = await fetch("/api/system/update-announcement", { cache: "no-store" }); - if (!res.ok) return; - const data = (await res.json()) as UpdateAnnouncementPayload; - if (!cancelled) { - setAnnouncement(data?.enabled ? data : null); - } - } catch { - if (!cancelled) setAnnouncement(null); - } - } - void loadAnnouncement(); - return () => { - cancelled = true; - }; - }, []); - useEffect(() => { if (!open) return; const handlePointerDown = (event: PointerEvent) => { @@ -77,13 +86,17 @@ export function UpdateAnnouncementButton({ isEn }: UpdateAnnouncementButtonProps return () => window.removeEventListener("pointerdown", handlePointerDown); }, [open]); + const announcement = useMemo( + () => STATIC_UPDATE_ANNOUNCEMENTS.find((item) => isActiveAnnouncement(item)) ?? null, + [], + ); const text = useMemo( () => (announcement ? pickAnnouncementText(announcement, isEn) : { title: "", body: "" }), [announcement, isEn], ); const updatedAt = useMemo( - () => formatUpdatedAt(announcement?.updated_at, isEn), - [announcement?.updated_at, isEn], + () => formatUpdatedAt(announcement?.publishedAt, isEn), + [announcement?.publishedAt, isEn], ); if (!announcement || (!text.title && !text.body)) return null; diff --git a/frontend/components/dashboard/scan-terminal/__tests__/updateAnnouncement.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/updateAnnouncement.test.ts index d180b7f7..13909a00 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/updateAnnouncement.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/updateAnnouncement.test.ts @@ -38,10 +38,9 @@ export function runTests() { const middlewareSource = fs.readFileSync(path.join(projectRoot, "middleware.ts"), "utf8"); assert(fs.existsSync(componentPath), "terminal must have a compact update announcement component"); - assert(fs.existsSync(nextRoutePath), "frontend must proxy the public update announcement API"); + assert(!fs.existsSync(nextRoutePath), "update announcements should not depend on an admin-managed API proxy"); const componentSource = fs.readFileSync(componentPath, "utf8"); - const routeSource = fs.readFileSync(nextRoutePath, "utf8"); assert( dashboardSource.includes("UpdateAnnouncementButton") && @@ -50,43 +49,41 @@ export function runTests() { "terminal header must render a bilingual update announcement entry beside the dashboard title", ); assert( - componentSource.includes("/api/system/update-announcement") && + componentSource.includes("STATIC_UPDATE_ANNOUNCEMENTS") && + componentSource.includes("expiresAt") && + componentSource.includes("Date.now()") && componentSource.includes("Megaphone") && componentSource.includes("zh") && componentSource.includes("en") && + !componentSource.includes("fetch(") && + !componentSource.includes("/api/system/update-announcement") && !componentSource.includes("setInterval("), - "announcement component must fetch the public announcement once, support zh/en content, and avoid aggressive polling", + "announcement component must use hardcoded zh/en release notes with an expiry time and no backend polling", ); assert( - routeSource.includes(`${"api/system/update-announcement"}`) && - routeSource.includes("cache: \"no-store\""), - "Next.js announcement proxy must call the backend public endpoint without caching stale admin content", + !middlewareSource.includes("/api/system/update-announcement"), + "middleware should not keep a public announcement API entry after announcements move into frontend code", ); assert( - middlewareSource.includes('pathname === "/api/system/update-announcement"'), - "update announcement proxy must stay public because it only returns non-sensitive release notes", + !opsConfigSource.includes("公告类配置") && + !opsConfigSource.includes("multiline") && + !opsConfigSource.includes(" null)) as Partial | null; setResult(`${key} 已更新`); - setConfigs((prev) => prev.map((c) => ( - c.key === key - ? { ...c, ...(data ?? {}), value: String(data?.value ?? newVal) } - : c - ))); + setConfigs((prev) => prev.map((c) => (c.key === key ? { ...c, value: newVal } : c))); setEditing((prev) => { const n = { ...prev }; delete n[key]; return n; }); } else { setResult(`保存失败: ${await res.text().catch(() => "")}`); @@ -149,89 +139,28 @@ export function ConfigPageClient() {

配置 API 尚未就绪(需要后端支持)

) : (
- {configs.map((cfg) => { - const currentValue = editing[cfg.key] ?? cfg.value; - const dirty = editing[cfg.key] != null && editing[cfg.key] !== cfg.value; - const persistent = Boolean(cfg.persistent || cfg.source === "runtime_config"); - const sourceLabel = persistent - ? "DB 持久化" - : cfg.source === "environment" - ? "环境变量" - : cfg.source || "当前进程"; - - if (persistent) { - return ( -
-
-
-
-
{cfg.key}
- - {sourceLabel} - -
-
{cfg.description}
-
- 更新人 {cfg.updated_by || "-"} - 更新时间 {cfg.updated_at || "-"} -
-
-
- {cfg.multiline ? ( -