Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export default function App() {
</button>
</div>
<nav>
{NAV.map(({ id, tkey, Icon }) => (
{NAV.filter(({ id }) => !(viewMode === "workspace" && id === "codex-auth")).map(({ id, tkey, Icon }) => (
<div key={id} className={`nav-entry${id === "claude" ? ` nav-entry-claude${page === id ? " active" : ""}` : ""}`}>
<button className={`nav-item${page === id ? " active" : ""}`} data-page={id}
onClick={() => {
Expand Down Expand Up @@ -281,7 +281,7 @@ export default function App() {
{page === "combos" && <Combos apiBase={API_BASE} />}
{page === "subagents" && <Subagents apiBase={API_BASE} viewMode={viewMode} />}
{page === "logs" && <Logs apiBase={API_BASE} />}
{page === "usage" && <Usage apiBase={API_BASE} />}
{page === "usage" && <Usage apiBase={API_BASE} viewMode={viewMode} />}
{page === "storage" && <Storage apiBase={API_BASE} />}
{page === "codex-auth" && <CodexAuth apiBase={API_BASE} />}
{page === "api" && <ApiKeys apiBase={API_BASE} />}
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,9 +548,12 @@ export const de = {
"usage.card.coverage": "Abdeckung",
"usage.card.activeDays": "Aktive Tage",
"usage.section.heatmap": "Tägliche Aktivität",
"usage.section.overview": "Übersicht",
"usage.section.models": "Modelle",
"usage.section.providers": "Anbieter",
"usage.section.coverage": "Abdeckungs-Aufschlüsselung",
"usage.workspace.sections": "Nutzungsabschnitte",
"usage.workspace.report": "Nutzungsbericht",
"usage.coverage.measured": "Gemessen",
"usage.coverage.reported": "Anbieter gemeldet",
"usage.coverage.estimated": "Geschätzt",
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,9 +572,12 @@ export const en = {
"usage.card.coverage": "Coverage",
"usage.card.activeDays": "Active days",
"usage.section.heatmap": "Daily activity",
"usage.section.overview": "Overview",
"usage.section.models": "Models",
"usage.section.providers": "Providers",
"usage.section.coverage": "Coverage breakdown",
"usage.workspace.sections": "Usage sections",
"usage.workspace.report": "Usage report",
"usage.coverage.measured": "Measured",
"usage.coverage.reported": "Provider reported",
"usage.coverage.estimated": "Estimated",
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,12 @@ export const ja: Record<TKey, string> = {
"usage.card.coverage": "カバレッジ",
"usage.card.activeDays": "アクティブ日数",
"usage.section.heatmap": "日のアクティビティ",
"usage.section.overview": "概要",
"usage.section.models": "モデル",
"usage.section.providers": "プロバイダー",
"usage.section.coverage": "カバレッジ内訳",
"usage.workspace.sections": "使用量セクション",
"usage.workspace.report": "使用量レポート",
"usage.coverage.measured": "計測",
"usage.coverage.reported": "プロバイダー報告",
"usage.coverage.estimated": "推定",
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,9 +565,12 @@ export const ko: Record<TKey, string> = {
"usage.card.coverage": "커버리지",
"usage.card.activeDays": "활동일",
"usage.section.heatmap": "일별 활동",
"usage.section.overview": "개요",
"usage.section.models": "모델",
"usage.section.providers": "프로바이더",
"usage.section.coverage": "커버리지 상세",
"usage.workspace.sections": "사용량 섹션",
"usage.workspace.report": "사용량 보고서",
"usage.coverage.measured": "측정됨",
"usage.coverage.reported": "제공자 보고",
"usage.coverage.estimated": "추정",
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,9 +572,12 @@ export const ru: Record<TKey, string> = {
"usage.card.coverage": "Покрытие",
"usage.card.activeDays": "Активные дни",
"usage.section.heatmap": "Активность по дням",
"usage.section.overview": "Обзор",
"usage.section.models": "Модели",
"usage.section.providers": "Провайдеры",
"usage.section.coverage": "Детализация покрытия",
"usage.workspace.sections": "Разделы использования",
"usage.workspace.report": "Отчёт об использовании",
"usage.coverage.measured": "Измерено",
"usage.coverage.reported": "Сообщено провайдером",
"usage.coverage.estimated": "Оценено",
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,9 +565,12 @@ export const zh: Record<TKey, string> = {
"usage.card.coverage": "覆盖率",
"usage.card.activeDays": "活跃天数",
"usage.section.heatmap": "每日活动",
"usage.section.overview": "概览",
"usage.section.models": "模型",
"usage.section.providers": "提供方",
"usage.section.coverage": "覆盖率明细",
"usage.workspace.sections": "用量分区",
"usage.workspace.report": "用量报告",
"usage.coverage.measured": "已计量",
"usage.coverage.reported": "提供方上报",
"usage.coverage.estimated": "估算",
Expand Down
15 changes: 9 additions & 6 deletions gui/src/pages/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,26 +277,30 @@ export default function Logs({ apiBase }: { apiBase: string }) {
else if (e.key === "ArrowRight" || e.key === "End") { e.preventDefault(); selectTab("debug"); document.getElementById("logs-tab-debug")?.focus(); }
};

const fetchLogs = useCallback(async () => {
setLoading(true);
setError(null);
const fetchLogs = useCallback(async (opts?: { silent?: boolean }) => {
const silent = opts?.silent === true;
// Silent polls must not clear an existing error or toggle loading — otherwise
// failures flicker between the error banner, empty state, and stale table.
if (!silent) setLoading(true);
try {
const res = await fetch(`${apiBase}/api/logs`);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim());
setLogs(await res.json());
setError(null);
} catch (cause) {
if (silent) return;
const detail = cause instanceof Error ? cause.message : "";
setError(detail ? `${t("logs.loadError")} ${detail}` : t("logs.loadError"));
} finally {
setLoading(false);
if (!silent) setLoading(false);
}
}, [apiBase, t]);
Comment on lines +280 to 297

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No cancellation/sequencing for polling fetch — stale response can overwrite fresher state.

fetchLogs fires a bare fetch(${apiBase}/api/logs) on every call (initial, retry, and every 2s silent poll) with no AbortController and no "latest request wins" guard. If two overlapping requests resolve out of order (e.g., a slow non-silent retry started at t=0 finishes after a silent poll fired at t=2000 already updated logs), the older response's setLogs/setError call will win and clobber the newer state. There's also no timeout, so a hung request leaves loading (and the disabled Retry button) stuck indefinitely, and repeated intervals can pile up in-flight requests against a slow backend.

Add an AbortController per call, abort the previous one before starting a new one (and on cleanup), and ignore AbortError in the catch:

🔧 Suggested fix
+  const fetchAbortRef = useRef<AbortController | null>(null);
   const fetchLogs = useCallback(async (opts?: { silent?: boolean }) => {
     const silent = opts?.silent === true;
+    fetchAbortRef.current?.abort();
+    const controller = new AbortController();
+    fetchAbortRef.current = controller;
     if (!silent) setLoading(true);
     try {
-      const res = await fetch(`${apiBase}/api/logs`);
+      const res = await fetch(`${apiBase}/api/logs`, { signal: controller.signal });
       if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim());
       setLogs(await res.json());
       setError(null);
     } catch (cause) {
+      if (cause instanceof DOMException && cause.name === "AbortError") return;
       if (silent) return;
       const detail = cause instanceof Error ? cause.message : "";
       setError(detail ? `${t("logs.loadError")} ${detail}` : t("logs.loadError"));
     } finally {
       if (!silent) setLoading(false);
     }
   }, [apiBase, t]);

Also applies to: 299-305

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/pages/Logs.tsx` around lines 280 - 297, Update fetchLogs to maintain
an AbortController for the active request, aborting the previous request before
starting each new call and aborting it during the owning effect cleanup. Pass
the signal to fetch, ignore AbortError in the catch path, and ensure aborted or
stale requests cannot update logs, errors, or loading state.


useEffect(() => {
if (tab !== "logs") return;
void fetchLogs();
if (!autoRefresh) return;
const interval = setInterval(() => void fetchLogs(), 2000);
const interval = setInterval(() => void fetchLogs({ silent: true }), 2000);
return () => clearInterval(interval);
}, [autoRefresh, fetchLogs, tab]);

Expand Down Expand Up @@ -402,7 +406,6 @@ export default function Logs({ apiBase }: { apiBase: string }) {
<EmptyState title={t("logs.noRequests")} />
) : (
<>
{loading && <div className="row muted" role="status"><span className="spin" /> {t("common.loading")}</div>}
<div ref={scrollContainerRef} className="tbl-wrap" style={{ overflowY: "auto", maxHeight: "calc(100vh - 260px)" }}>
<table className="tbl logs-table">
<thead style={{ position: "sticky", top: 0, zIndex: 1, background: "var(--surface)" }}>
Expand Down
Loading
Loading