From 8f6925108c499c27e96f3622520c003670825cef Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 8 Jun 2026 15:12:41 +0800 Subject: [PATCH] Reduce polling and batch observation writes --- .../dashboard/ScanTerminalDashboard.tsx | 6 +- .../UserFeedbackStatusButton.tsx | 70 +++++++++++++++++-- .../feedbackStatusNotification.test.ts | 21 +++++- .../__tests__/refreshCadencePolicy.test.ts | 6 ++ .../scan-terminal/feedback-status.ts | 3 +- src/data_collection/weather_sources.py | 16 +++-- src/database/db_manager.py | 46 ++++++++++-- tests/test_api_performance_timing.py | 26 +++++++ tests/test_observation_collector.py | 35 ++++++++++ tests/test_web_observability.py | 51 ++++++++++++++ web/services/auth_api.py | 2 +- web/services/request_timing.py | 23 ++++++ 12 files changed, 281 insertions(+), 24 deletions(-) diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index c3ee89f7..75f49e4b 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -102,6 +102,8 @@ const TERMINAL_NAV_ITEMS = [ ] as const; const AUTH_PROFILE_REQUEST_TIMEOUT_MS = 4500; const AUTH_DECISION_RECOVERY_MS = 10_000; +const AUTH_PROFILE_RETRY_INITIAL_DELAY_MS = 5_000; +const AUTH_PROFILE_RETRY_POLL_MS = 30_000; const ACTIVE_ACCESS_CACHE_KEY = "polyweather_terminal_active_access_v1"; const ACTIVE_ACCESS_CACHE_TTL_MS = 6 * 60 * 60 * 1000; @@ -1554,10 +1556,10 @@ function ScanTerminalScreen() { const firstRetry = window.setTimeout(() => { void retryAuthProfile(); - }, 1500); + }, AUTH_PROFILE_RETRY_INITIAL_DELAY_MS); const interval = window.setInterval(() => { void retryAuthProfile(); - }, 5000); + }, AUTH_PROFILE_RETRY_POLL_MS); return () => { cancelled = true; window.clearTimeout(firstRetry); diff --git a/frontend/components/dashboard/scan-terminal/UserFeedbackStatusButton.tsx b/frontend/components/dashboard/scan-terminal/UserFeedbackStatusButton.tsx index a248bb59..de1e74aa 100644 --- a/frontend/components/dashboard/scan-terminal/UserFeedbackStatusButton.tsx +++ b/frontend/components/dashboard/scan-terminal/UserFeedbackStatusButton.tsx @@ -1,17 +1,19 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Bell, RefreshCcw } from "lucide-react"; import type { UserFeedbackEntry, UserFeedbackPayload } from "@/types/ops"; import { buildFeedbackNotificationKey, countUnseenFeedbackUpdates, + FEEDBACK_STATUS_CACHE_TTL_MS, FEEDBACK_STATUS_POLL_MS, feedbackStatusLabel, feedbackStatusTone, } from "./feedback-status"; const FEEDBACK_STATUS_SEEN_KEY = "polyweather_feedback_status_seen_v1"; +const FEEDBACK_STATUS_CACHE_KEY = "polyweather_feedback_status_cache_v1"; function loadSeenKeys() { if (typeof window === "undefined") return new Set(); @@ -34,6 +36,33 @@ function saveSeenKeys(keys: Set) { } } +function readFeedbackStatusCache(): { entries: UserFeedbackEntry[]; ts: number } | null { + if (typeof window === "undefined") return null; + try { + const raw = window.localStorage.getItem(FEEDBACK_STATUS_CACHE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + const ts = Number(parsed?.ts || 0); + const entries = Array.isArray(parsed?.entries) ? parsed.entries : []; + if (!ts || Date.now() - ts > FEEDBACK_STATUS_CACHE_TTL_MS) return null; + return { entries, ts }; + } catch { + return null; + } +} + +function writeFeedbackStatusCache(entries: UserFeedbackEntry[]) { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem( + FEEDBACK_STATUS_CACHE_KEY, + JSON.stringify({ entries: entries.slice(0, 20), ts: Date.now() }), + ); + } catch { + // Ignore storage failures; status will still refresh over the network. + } +} + function compactDate(value?: string) { if (!value) return ""; return value.slice(0, 16).replace("T", " "); @@ -57,11 +86,12 @@ export function UserFeedbackStatusButton({ refreshKey?: number; }) { const [available, setAvailable] = useState(true); - const [entries, setEntries] = useState([]); + const [entries, setEntries] = useState(() => readFeedbackStatusCache()?.entries || []); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const [seenKeys, setSeenKeys] = useState>(() => loadSeenKeys()); + const lastLoadedAtRef = useRef(readFeedbackStatusCache()?.ts || 0); const unseenCount = useMemo( () => countUnseenFeedbackUpdates(entries, seenKeys), @@ -75,8 +105,26 @@ export function UserFeedbackStatusButton({ ? "No submitted feedback yet." : "暂无已提交反馈。"; - const load = useCallback(async (signal?: AbortSignal) => { + const load = useCallback(async ( + signal?: AbortSignal, + options?: { force?: boolean }, + ) => { if (typeof fetch !== "function") return; + const cached = readFeedbackStatusCache(); + if (!options?.force && cached) { + lastLoadedAtRef.current = cached.ts; + setEntries(cached.entries); + setAvailable(true); + return; + } + const now = Date.now(); + if ( + !options?.force && + lastLoadedAtRef.current && + now - lastLoadedAtRef.current < FEEDBACK_STATUS_CACHE_TTL_MS + ) { + return; + } setLoading(true); try { const res = await fetch("/api/feedback?limit=12", { @@ -93,7 +141,10 @@ export function UserFeedbackStatusButton({ throw new Error(`HTTP ${res.status}`); } const payload = (await res.json()) as UserFeedbackPayload; - setEntries(Array.isArray(payload.feedback) ? payload.feedback : []); + const nextEntries = Array.isArray(payload.feedback) ? payload.feedback : []; + setEntries(nextEntries); + writeFeedbackStatusCache(nextEntries); + lastLoadedAtRef.current = Date.now(); setAvailable(true); setError(""); } catch (err) { @@ -115,10 +166,15 @@ export function UserFeedbackStatusButton({ useEffect(() => { const controller = new AbortController(); - void load(controller.signal); + void load(controller.signal, { force: refreshKey > 0 }); const handleVisibilityChange = () => { - if (document.visibilityState === "visible") void load(); + if ( + document.visibilityState === "visible" && + Date.now() - lastLoadedAtRef.current >= FEEDBACK_STATUS_CACHE_TTL_MS + ) { + void load(); + } }; document.addEventListener("visibilitychange", handleVisibilityChange); const id = window.setInterval(() => { @@ -174,7 +230,7 @@ export function UserFeedbackStatusButton({