Skip to content

Commit b968762

Browse files
committed
fix(gui): separate logs and usage error states
1 parent cf07d29 commit b968762

8 files changed

Lines changed: 77 additions & 21 deletions

File tree

gui/src/i18n/de.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const de = {
1515
"common.discard": "Verwerfen",
1616
"common.remove": "Entfernen",
1717
"common.loading": "Lädt…",
18+
"common.retry": "Wiederholen",
1819
"theme.label": "Design",
1920
"theme.light": "Hell",
2021
"theme.dark": "Dunkel",
@@ -401,6 +402,7 @@ export const de = {
401402
"logs.subtitle": "Letzte Anfragen über den lokalen opencodex-Proxy, neueste zuerst.",
402403
"logs.autoRefresh": "Auto-Aktualisierung",
403404
"logs.noRequests": "Noch keine Anfragen.",
405+
"logs.loadError": "Anfrageprotokolle konnten nicht geladen werden.",
404406
"logs.filter.surface.label": "Oberfläche",
405407
"logs.filter.surface.all": "Alle",
406408
"logs.filter.surface.claude": "Claude",
@@ -502,6 +504,7 @@ export const de = {
502504
"usage.subtitle": "Lokale Token-Buchhaltung deines Proxys. Fehlende Nutzung wird nie als Null angezeigt.",
503505
"usage.loading": "Lade Nutzungsdaten…",
504506
"usage.empty": "Noch keine Nutzung erfasst. Sende eine Anfrage über den Proxy, um Aktivität hier zu sehen.",
507+
"usage.loadError": "Nutzungsdaten konnten nicht geladen werden.",
505508
"usage.range.all": "Alle",
506509
"usage.range.30d": "30d",
507510
"usage.range.7d": "7d",

gui/src/i18n/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const en = {
1919
"common.ok": "OK",
2020
"common.remove": "Remove",
2121
"common.loading": "Loading…",
22+
"common.retry": "Retry",
2223
"app.logoAria": "opencodex logo",
2324
"app.claudeOn": "Claude ON",
2425
"app.claudeOff": "Claude OFF",
@@ -420,6 +421,7 @@ export const en = {
420421
"logs.subtitle": "Recent requests routed through the local opencodex proxy, newest first.",
421422
"logs.autoRefresh": "Auto-refresh",
422423
"logs.noRequests": "No requests yet.",
424+
"logs.loadError": "Could not load request logs.",
423425
"logs.filter.surface.label": "Surface",
424426
"logs.filter.surface.all": "All",
425427
"logs.filter.surface.claude": "Claude",
@@ -525,6 +527,7 @@ export const en = {
525527
"usage.subtitle": "Local token accounting from your proxy. Missing usage is never shown as zero.",
526528
"usage.loading": "Loading usage data…",
527529
"usage.empty": "No usage recorded yet. Send a request through the proxy to see activity here.",
530+
"usage.loadError": "Could not load usage data.",
528531
"usage.range.all": "All",
529532
"usage.range.30d": "30d",
530533
"usage.range.7d": "7d",

gui/src/i18n/ja.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const ja: Record<TKey, string> = {
1919
"common.ok": "OK",
2020
"common.remove": "削除",
2121
"common.loading": "読み込み中…",
22+
"common.retry": "再試行",
2223
"app.logoAria": "opencodex ロゴ",
2324
"app.claudeOn": "Claude オン",
2425
"app.claudeOff": "Claude オフ",
@@ -385,6 +386,7 @@ export const ja: Record<TKey, string> = {
385386
"logs.subtitle": "ローカル opencodex プロキシを経由した最近のリクエスト(新しい順)。",
386387
"logs.autoRefresh": "自動更新",
387388
"logs.noRequests": "まだリクエストがありません。",
389+
"logs.loadError": "リクエストログを読み込めませんでした。",
388390
"logs.filter.surface.label": "サーフェス",
389391
"logs.filter.surface.all": "すべて",
390392
"logs.filter.surface.claude": "Claude",
@@ -490,6 +492,7 @@ export const ja: Record<TKey, string> = {
490492
"usage.subtitle": "プロキシからのローカルトークン会計です。欠損した使用量はゼロとして表示されることはありません。",
491493
"usage.loading": "使用量データを読み込み中…",
492494
"usage.empty": "まだ使用量が記録されていません。プロキシ経由でリクエストを送信するとここにアクティビティが表示されます。",
495+
"usage.loadError": "使用量データを読み込めませんでした。",
493496
"usage.range.all": "すべて",
494497
"usage.range.30d": "30日",
495498
"usage.range.7d": "7日",

gui/src/i18n/ko.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const ko: Record<TKey, string> = {
1717
"common.discard": "버리기",
1818
"common.remove": "삭제",
1919
"common.loading": "불러오는 중…",
20+
"common.retry": "재시도",
2021
"theme.label": "테마",
2122
"theme.light": "라이트",
2223
"theme.dark": "다크",
@@ -415,6 +416,7 @@ export const ko: Record<TKey, string> = {
415416
"logs.subtitle": "로컬 opencodex 프록시를 거친 최근 요청입니다. 최신순.",
416417
"logs.autoRefresh": "자동 새로고침",
417418
"logs.noRequests": "아직 요청이 없습니다.",
419+
"logs.loadError": "요청 로그를 불러오지 못했습니다.",
418420
"logs.filter.surface.label": "표면",
419421
"logs.filter.surface.all": "전체",
420422
"logs.filter.surface.claude": "Claude",
@@ -519,6 +521,7 @@ export const ko: Record<TKey, string> = {
519521
"usage.subtitle": "프록시의 로컬 토큰 집계입니다. 누락된 사용량은 0으로 표시하지 않습니다.",
520522
"usage.loading": "사용량 데이터를 불러오는 중…",
521523
"usage.empty": "아직 기록된 사용량이 없습니다. 프록시로 요청을 보내면 여기에 표시됩니다.",
524+
"usage.loadError": "사용량 데이터를 불러오지 못했습니다.",
522525
"usage.range.all": "전체",
523526
"usage.range.30d": "30일",
524527
"usage.range.7d": "7일",

gui/src/i18n/ru.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const ru: Record<TKey, string> = {
1919
"common.ok": "ОК",
2020
"common.remove": "Удалить",
2121
"common.loading": "Загрузка…",
22+
"common.retry": "Повторить",
2223
"app.logoAria": "Логотип opencodex",
2324
"app.claudeOn": "Claude ВКЛ",
2425
"app.claudeOff": "Claude ВЫКЛ",
@@ -420,6 +421,7 @@ export const ru: Record<TKey, string> = {
420421
"logs.subtitle": "Недавние запросы через локальный прокси opencodex, новые сверху.",
421422
"logs.autoRefresh": "Автообновление",
422423
"logs.noRequests": "Запросов пока нет.",
424+
"logs.loadError": "Не удалось загрузить журнал запросов.",
423425
"logs.filter.surface.label": "Источник",
424426
"logs.filter.surface.all": "Все",
425427
"logs.filter.surface.claude": "Claude",
@@ -525,6 +527,7 @@ export const ru: Record<TKey, string> = {
525527
"usage.subtitle": "Локальный учёт токенов вашего прокси. Отсутствующие данные никогда не показываются как ноль.",
526528
"usage.loading": "Загрузка данных об использовании…",
527529
"usage.empty": "Данных об использовании пока нет. Отправьте запрос через прокси, чтобы увидеть здесь активность.",
530+
"usage.loadError": "Не удалось загрузить данные об использовании.",
528531
"usage.range.all": "Все",
529532
"usage.range.30d": "30 дн.",
530533
"usage.range.7d": "7 дн.",

gui/src/i18n/zh.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const zh: Record<TKey, string> = {
1717
"common.discard": "丢弃",
1818
"common.remove": "移除",
1919
"common.loading": "加载中…",
20+
"common.retry": "重试",
2021
"theme.label": "主题",
2122
"theme.light": "浅色",
2223
"theme.dark": "深色",
@@ -415,6 +416,7 @@ export const zh: Record<TKey, string> = {
415416
"logs.subtitle": "经过本地 opencodex 代理的最近请求,最新在前。",
416417
"logs.autoRefresh": "自动刷新",
417418
"logs.noRequests": "暂无请求。",
419+
"logs.loadError": "无法加载请求日志。",
418420
"logs.filter.surface.label": "界面",
419421
"logs.filter.surface.all": "全部",
420422
"logs.filter.surface.claude": "Claude",
@@ -519,6 +521,7 @@ export const zh: Record<TKey, string> = {
519521
"usage.subtitle": "代理本地的 Token 用量统计。缺失的用量不会显示为零。",
520522
"usage.loading": "正在加载用量数据…",
521523
"usage.empty": "尚无用量记录。通过代理发送请求后将在此显示。",
524+
"usage.loadError": "无法加载用量数据。",
522525
"usage.range.all": "全部",
523526
"usage.range.30d": "30 天",
524527
"usage.range.7d": "7 天",

gui/src/pages/Logs.tsx

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { useEffect, useRef, useState } from "react";
1+
import { useCallback, useEffect, useRef, useState } from "react";
22
import { useVirtualizer } from "@tanstack/react-virtual";
33
import { useI18n, LOCALES, type TFn } from "../i18n/shared";
44
import { formatTokens } from "../format-tokens";
55
import { statusCodeInfo } from "../status-codes";
66
import { IconX } from "../icons";
77
import { modelLabel } from "../model-display";
8-
import { EmptyState } from "../ui";
8+
import { EmptyState, Notice } from "../ui";
99
import Debug from "./Debug";
1010

1111
type LogsTab = "logs" | "debug";
@@ -251,6 +251,8 @@ function modelTitle(log: LogEntry): string {
251251
export default function Logs({ apiBase }: { apiBase: string }) {
252252
const { t, locale } = useI18n();
253253
const [logs, setLogs] = useState<LogEntry[]>([]);
254+
const [loading, setLoading] = useState(true);
255+
const [error, setError] = useState<string | null>(null);
254256
const [autoRefresh, setAutoRefresh] = useState(true);
255257
const [detail, setDetail] = useState<LogEntry | null>(null);
256258
const [surfaceFilter, setSurfaceFilter] = useState<"all" | "claude" | "codex">("all");
@@ -275,19 +277,28 @@ export default function Logs({ apiBase }: { apiBase: string }) {
275277
else if (e.key === "ArrowRight" || e.key === "End") { e.preventDefault(); selectTab("debug"); document.getElementById("logs-tab-debug")?.focus(); }
276278
};
277279

280+
const fetchLogs = useCallback(async () => {
281+
setLoading(true);
282+
setError(null);
283+
try {
284+
const res = await fetch(`${apiBase}/api/logs`);
285+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim());
286+
setLogs(await res.json());
287+
} catch (cause) {
288+
const detail = cause instanceof Error ? cause.message : "";
289+
setError(detail ? `${t("logs.loadError")} ${detail}` : t("logs.loadError"));
290+
} finally {
291+
setLoading(false);
292+
}
293+
}, [apiBase, t]);
294+
278295
useEffect(() => {
279296
if (tab !== "logs") return;
280-
const fetchLogs = async () => {
281-
try {
282-
const res = await fetch(`${apiBase}/api/logs`);
283-
setLogs(await res.json());
284-
} catch { /* ignore */ }
285-
};
286-
fetchLogs();
297+
void fetchLogs();
287298
if (!autoRefresh) return;
288-
const interval = setInterval(fetchLogs, 2000);
299+
const interval = setInterval(() => void fetchLogs(), 2000);
289300
return () => clearInterval(interval);
290-
}, [apiBase, autoRefresh, tab]);
301+
}, [autoRefresh, fetchLogs, tab]);
291302

292303
const detailInfo = detail ? statusCodeInfo(detail.status, locale) : null;
293304
const filteredLogs = logs.filter(log => (
@@ -378,9 +389,20 @@ export default function Logs({ apiBase }: { apiBase: string }) {
378389
</div>
379390
</div>
380391

381-
{filteredLogs.length === 0 ? (
392+
{error ? (
393+
<Notice tone="err">
394+
{error}{" "}
395+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => void fetchLogs()} disabled={loading}>
396+
{t("common.retry")}
397+
</button>
398+
</Notice>
399+
) : loading && logs.length === 0 ? (
400+
<EmptyState title={t("common.loading")} />
401+
) : filteredLogs.length === 0 ? (
382402
<EmptyState title={t("logs.noRequests")} />
383403
) : (
404+
<>
405+
{loading && <div className="row muted" role="status"><span className="spin" /> {t("common.loading")}</div>}
384406
<div ref={scrollContainerRef} className="tbl-wrap" style={{ overflowY: "auto", maxHeight: "calc(100vh - 260px)" }}>
385407
<table className="tbl logs-table">
386408
<thead style={{ position: "sticky", top: 0, zIndex: 1, background: "var(--surface)" }}>
@@ -481,6 +503,7 @@ export default function Logs({ apiBase }: { apiBase: string }) {
481503
</tbody>
482504
</table>
483505
</div>
506+
</>
484507
)}
485508

486509
{detail && (

gui/src/pages/Usage.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
22
import { useI18n, type TFn, type Locale } from "../i18n/shared";
33
import { formatTokens } from "../format-tokens";
4-
import { EmptyState } from "../ui";
4+
import { EmptyState, Notice } from "../ui";
55
import { modelLabel } from "../model-display";
66

77
type Range = "all" | "30d" | "7d";
@@ -554,24 +554,27 @@ export default function Usage({ apiBase }: { apiBase: string }) {
554554
const [surface, setSurface] = useState<UsageSurface>("all");
555555
const [data, setData] = useState<UsageResponse | null>(null);
556556
const [loading, setLoading] = useState(true);
557+
const [error, setError] = useState<string | null>(null);
557558
const [modelQuery, setModelQuery] = useState("");
558559

559560
const fetchUsage = useCallback(async (nextRange: Range, nextSurface: UsageSurface, signal: AbortSignal) => {
560561
setLoading(true);
562+
setError(null);
561563
try {
562564
const res = await fetch(`${apiBase}/api/usage?range=${nextRange}&surface=${nextSurface}`, { signal });
563-
if (!res.ok) throw new Error("fetch failed");
565+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim());
564566
const json = await res.json() as UsageResponse;
565567
if (signal.aborted) return;
566568
setData(json);
567-
} catch {
569+
} catch (cause) {
568570
// A stale request (range/apiBase changed, or unmount) must not overwrite newer state.
569571
if (signal.aborted) return;
570-
setData(null);
572+
const detail = cause instanceof Error ? cause.message : "";
573+
setError(detail ? `${t("usage.loadError")} ${detail}` : t("usage.loadError"));
571574
} finally {
572575
if (!signal.aborted) setLoading(false);
573576
}
574-
}, [apiBase]);
577+
}, [apiBase, t]);
575578

576579
useEffect(() => {
577580
const controller = new AbortController();
@@ -612,19 +615,31 @@ export default function Usage({ apiBase }: { apiBase: string }) {
612615
</div>
613616
<p className="page-sub">{t("usage.subtitle")}</p>
614617

615-
{loading && !data ? (
618+
{error ? (
619+
<Notice tone="err">
620+
{error}{" "}
621+
<button
622+
type="button"
623+
className="btn btn-ghost btn-sm"
624+
onClick={() => void fetchUsage(range, surface, new AbortController().signal)}
625+
disabled={loading}
626+
>
627+
{t("common.retry")}
628+
</button>
629+
</Notice>
630+
) : loading && !data ? (
616631
<EmptyState title={t("usage.loading")} />
617-
) : !data || data.summary.requests === 0 ? (
632+
) : data?.summary.requests === 0 ? (
618633
<EmptyState title={t("usage.empty")} />
619-
) : (
634+
) : data ? (
620635
<>
621636
<UsageSummaryCards summary={data.summary} activeDays={activeDays} locale={locale} t={t} />
622637
<UsageHeatmapPanel range={range} heatmap={heatmap} weekBars={weekBars} locale={locale} t={t} />
623638
<UsageModelsTable models={filteredModels} modelQuery={modelQuery} onModelQuery={setModelQuery} locale={locale} t={t} />
624639
<UsageProvidersTable providers={sortedProviders} locale={locale} t={t} />
625640
<UsageCoveragePanel summary={data.summary} t={t} />
626641
</>
627-
)}
642+
) : null}
628643
</>
629644
);
630645
}

0 commit comments

Comments
 (0)