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
17 changes: 9 additions & 8 deletions app/admin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,16 +536,17 @@ async def logs_clear(request):

@route("GET", "/admin/storage")
async def storage_get(request):
# A degraded store still reports its own health; a 503 here would hide
# the diagnostics this endpoint exists to surface.
result = await run_in_threadpool(audit.storage)
if result.get("degraded"):
return error_response(503, "存储状态暂时无法读取,请检查审计存储")
archives = []
try:
directory = Path(str(audit.path)).parent / "archives"
archives = sorted((entry.name for entry in directory.glob("audit-*.jsonl")), reverse=True)[:20]
except OSError as exc:
audit._fault(exc)
return error_response(503, "存储状态暂时无法读取,请检查审计存储")
path = getattr(audit, "path", None)
if path:
try:
directory = Path(str(path)).parent / "archives"
archives = sorted((entry.name for entry in directory.glob("audit-*.jsonl")), reverse=True)[:20]
except OSError as exc:
audit._fault(exc)
return JSONResponse({**result, "archives": archives})

@route("POST", "/admin/storage/archive")
Expand Down
210 changes: 138 additions & 72 deletions app/audit_store.py

Large diffs are not rendered by default.

93 changes: 91 additions & 2 deletions tests/test_audit_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,19 @@ def test_sqlite_busy_has_bounded_wait_and_no_partial_ingest(self):
self.assertEqual(self.store.storage()["ingest_count"], 0)
self.assertTrue(self.store.storage()["degraded"])

def test_thread_lock_timeout_is_visible(self):
def test_thread_lock_timeout_is_transient_not_degraded(self):
# Contention is expected under load; it must never latch the 24h banner.
record = self.record()
with self.store._lock:
start = time.monotonic()
result = self.store.record_request(record)
self.assertLess(time.monotonic() - start, 1)
self.assertFalse(result["ok"])
self.assertEqual(self.store.storage()["last_error"], "TimeoutError")
health = self.store.storage()
self.assertFalse(health["degraded"])
self.assertEqual(health["transient_contention"], 1)
self.assertEqual(health["dropped_records"], 1)
self.assertIsNone(health["last_error"])

def test_transient_fault_degrades_only_within_window(self):
with patch.object(self.store, "_aggregate", side_effect=sqlite3.OperationalError("boom")):
Expand All @@ -218,6 +223,90 @@ def test_transient_fault_degrades_only_within_window(self):
self.assertEqual(health["dropped_records"], 1)
self.assertEqual(health["last_error"], "OperationalError")

def test_reads_do_not_queue_behind_write_transactions(self):
# The WebUI fires parallel dashboard/log/storage reads while inference
# writes run; reads take a WAL snapshot, never the write lock.
for i in range(3):
self.assertTrue(self.store.record_request(
self.record(f"r{i}", started_at=time.time() - i, duration_ms=100 + i))["recorded"])
with self.store._lock: # a write transaction in flight
self.assertEqual(self.store.ticket()["epoch"], 0)
self.assertEqual(self.store.dashboard(30)["summary"]["requests"], 3)
self.assertEqual(self.store.get_request("r0")["id"], "r0")
self.assertEqual(len(self.store.list_records("request", 10)["items"]), 3)
self.assertFalse(self.store.storage()["degraded"])
# Writers and readers share no lock; a fresh record commits immediately.
self.assertTrue(self.store.record_request(
self.record("w0", started_at=time.time()))["recorded"])
self.assertFalse(self.store.storage()["degraded"])

def test_dashboard_view_storm_does_not_drop_traffic_records(self):
# Regression: parallel dashboard queries used to steal the 250ms write
# lock budget of live traffic and latch a day of false storage alarms.
now = time.time()
payload = json.dumps({"id": "seed", "duration_ms": 5})
with self.store._lock:
self.store._db.execute("PRAGMA synchronous=OFF") # bulk seed only
self.store._db.execute("BEGIN IMMEDIATE")
self.store._db.executemany(
"INSERT INTO requests VALUES(?,?,?,?,?,?,?,?,?,?,?)",
[(f"s{i}", now - i, "m", "p", "c", "success", 200, payload, 64, None, 5.0)
for i in range(20000)])
self.store._db.execute("COMMIT")
self.store._db.execute("PRAGMA synchronous=FULL")

def traffic(tag):
dropped = 0
for i in range(20):
if not self.store.record_request(
self.record(f"{tag}{i}", started_at=time.time()))["ok"]:
dropped += 1
return dropped

with concurrent.futures.ThreadPoolExecutor(5) as pool:
futures = [pool.submit(self.store.dashboard, 30),
pool.submit(self.store.dashboard, 1, "hour"),
pool.submit(self.store.storage), pool.submit(traffic, "a"),
pool.submit(traffic, "b")]
dashboards = [futures[0].result(), futures[1].result()]
dropped = futures[3].result() + futures[4].result()
self.assertIsNone(dashboards[0].get("degraded"))
self.assertIsNone(dashboards[1].get("degraded"))
self.assertEqual(dropped, 0)
health = self.store.storage()
self.assertFalse(health["degraded"])
self.assertEqual(health["dropped_records"], 0)

def test_v3_duration_column_backfills_and_p95_uses_covered_index(self):
with tempfile.TemporaryDirectory() as td:
path = Path(td) / "audit.sqlite3"
store = AuditStore(path)
base = time.time() - 3 * 86400
for i in range(5):
self.assertTrue(store.record_request(
self.record(f"r{i}", started_at=base + i, duration_ms=100 + i))["recorded"])
store.close()
with closing(sqlite3.connect(str(path))) as db:
db.execute("DROP INDEX requests_duration")
db.execute("ALTER TABLE requests DROP COLUMN duration_ms")
db.execute("PRAGMA user_version=3")
db.commit()
migrated = AuditStore(path)
try:
with closing(sqlite3.connect(str(path))) as db:
self.assertEqual(db.execute("PRAGMA user_version").fetchone()[0], SCHEMA_VERSION)
rows = [r[0] for r in db.execute("SELECT duration_ms FROM requests ORDER BY id").fetchall()]
self.assertEqual(rows, [100.0, 101.0, 102.0, 103.0, 104.0])
with closing(migrated._open_read()) as plan_conn:
plan = plan_conn.execute(
"EXPLAIN QUERY PLAN SELECT duration_ms FROM requests WHERE started_at>=? AND started_at<=?",
(0, time.time())).fetchall()
self.assertIn("COVERING INDEX requests_duration", plan[0][3])
summary = migrated.dashboard(30)["summary"]
self.assertEqual(summary["requests"], 5)
self.assertEqual(summary["p95_duration_ms"], 104)
finally:
migrated.close()
def test_v1_ingest_table_migrates_with_backfilled_timestamps(self):
"""Migrate v1 ingest timestamps and indexes while preserving retained deduplication."""
with tempfile.TemporaryDirectory() as td:
Expand Down
3 changes: 3 additions & 0 deletions web/e2e/experience.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ test("daily range uses hourly points, with desktop and mobile glass layouts", as
await expect(page.getByRole("img", { name: "按小时用量趋势" })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
await page.screenshot({ path: "test-results/glass-overview-mobile.png" });
// The collapsed mobile sidebar stows the nav strip; only the expand bar remains.
await expect(page.getByRole("link", { name: "模型路由", exact: true })).toBeHidden();
await page.getByRole("button", { name: "展开侧栏" }).click();
await page.getByRole("link", { name: "模型路由", exact: true }).click();
await page.getByRole("button", { name: "新增模型" }).click();
await page.getByRole("radio", { name: /指定账号/ }).check();
Expand Down
23 changes: 13 additions & 10 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,6 @@ function Shell() {
CodeBuddy<small>{t("app.brandSubtitle")}</small>
</span>
</NavLink>
<button
className={s.sidebarToggle}
onClick={toggleSidebar}
aria-label={collapsed ? t("app.expandSidebar") : t("app.collapseSidebar")}
aria-expanded={!collapsed}
aria-controls="workspace-navigation"
title={collapsed ? t("app.expandSidebar") : t("app.collapseSidebar")}
>
<Icon name={collapsed ? "expand" : "collapse"} />
</button>
</div>
<p className={s.navLabel}>{t("app.workspace")}</p>
<nav id="workspace-navigation" aria-label={t("app.primaryNav")}>
Expand All @@ -113,6 +103,19 @@ function Shell() {
);
})}
</nav>
<button
className={s.sidebarBar}
onClick={toggleSidebar}
aria-label={collapsed ? t("app.expandSidebar") : t("app.collapseSidebar")}
aria-expanded={!collapsed}
aria-controls="workspace-navigation"
title={collapsed ? t("app.expandSidebar") : t("app.collapseSidebar")}
>
<span className={s.sidebarBarText}>
{collapsed ? t("app.expandSidebar") : t("app.collapseSidebar")}
</span>
<Icon name={collapsed ? "expand" : "collapse"} />
</button>
</aside>
<div className={s.workspace}>
<header className={s.header}>
Expand Down
12 changes: 12 additions & 0 deletions web/src/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ const zh: Record<string, string> = {
"logs.action.credential_checkin": "凭证签到",
"logs.action.credential_daily_chat": "凭证每日对话",
"logs.action.credential_auto_daily_chat": "凭证自动每日对话",
"logs.action.credential_auto_checkin": "凭证自动签到",
"logs.action.credential_auto_travel": "凭证自动旅行",
"logs.action.credential_trial": "凭证试用",
"logs.action.credential_enabled": "凭证启停",
"logs.action.credential_reset_cooldown": "凭证清除冷却",
Expand All @@ -146,6 +148,10 @@ const zh: Record<string, string> = {
"logs.action.model_created": "模型已创建",
"logs.action.model_updated": "模型已更新",
"logs.action.model_deleted": "模型已删除",
"logs.action.session_revoke_failed": "会话吊销失败",
"logs.action.credentials_uploaded": "凭证已上传",
"logs.action.credentials_exported": "凭证已导出",
"logs.action.storage_archive": "日志归档",
"dash.refresh": "刷新",
"dash.runtime": "运行概览",
"dash.trend": "请求趋势",
Expand Down Expand Up @@ -409,6 +415,8 @@ const en: Record<string, string> = {
"logs.action.credential_checkin": "Credential check-in",
"logs.action.credential_daily_chat": "Credential daily chat",
"logs.action.credential_auto_daily_chat": "Credential auto daily chat",
"logs.action.credential_auto_checkin": "Credential auto check-in",
"logs.action.credential_auto_travel": "Credential auto travel",
"logs.action.credential_trial": "Credential trial",
"logs.action.credential_enabled": "Credential enabled",
"logs.action.credential_reset_cooldown": "Credential cooldown reset",
Expand All @@ -417,6 +425,10 @@ const en: Record<string, string> = {
"logs.action.model_created": "Model created",
"logs.action.model_updated": "Model updated",
"logs.action.model_deleted": "Model deleted",
"logs.action.session_revoke_failed": "Session revoke failed",
"logs.action.credentials_uploaded": "Credentials uploaded",
"logs.action.credentials_exported": "Credentials exported",
"logs.action.storage_archive": "Log archive",
"dash.refresh": "Refresh",
"dash.runtime": "Runtime overview",
"dash.trend": "Request trend",
Expand Down
103 changes: 57 additions & 46 deletions web/src/pages/Credentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,51 @@ function ImportDrawer({ onClose, onDone }: { onClose: () => void; onDone: () =>
);
}

function MaintenanceResults({ results }: { results: Record<string, unknown>[] }) {
return (
<Panel title="凭证操作结果">
<ul className={s.operationResults} aria-live="polite">
{results.map((r, i) => {
const complete =
r.ok === true &&
!(r.travel && typeof r.travel === "object" && object(r.travel).ok === false);
return (
<li key={i}>
<strong>{text(r.name)}</strong>
<Badge tone={complete ? "good" : "warn"}>
{complete
? "已完成"
: r.checkin_ok === true ||
r.claimed === true ||
r.departed === true ||
r.buddy_claimed === true ||
r.agreement_accepted === true ||
(r.travel &&
typeof r.travel === "object" &&
(object(r.travel).claimed === true || object(r.travel).departed === true))
? "部分完成"
: r.skipped
? "已跳过"
: "未完成"}
</Badge>
<span>{text(r.message)}</span>
<TravelSummary
trip={
r.travel && typeof r.travel === "object"
? object(r.travel)
: r.action === "travel" || r.action === "travel-status"
? r
: null
}
/>
</li>
);
})}
</ul>
</Panel>
);
}

type ChannelKey = "all" | "cn" | "intl" | "enterprise";
type ViewMode = "cards" | "list";
type StatusFilter = "all" | "ready" | "issue" | "disabled";
Expand Down Expand Up @@ -720,14 +765,14 @@ export function Credentials() {
</>
}
/>
{notice && (
{!detail && notice && (
<p className={s.successNotice} role="status">
{notice}
</p>
)}
{busy && <p role="status">操作执行中,请勿重复提交…</p>}
{!detail && busy && <p role="status">操作执行中,请勿重复提交…</p>}
<ResourceState {...resource} />
<ErrorNotice message={error} />
{!detail && <ErrorNotice message={error} />}
{resource.data && (
<div className={s.channelGrid} role="group" aria-label="区域与账号类型筛选">
{channelDefs.map(([key, label, hint]) => {
Expand Down Expand Up @@ -783,49 +828,7 @@ export function Credentials() {
})}
</div>
)}
{maintenance.length > 0 && (
<Panel title="凭证操作结果">
<ul className={s.operationResults} aria-live="polite">
{maintenance.map((r, i) => {
const complete =
r.ok === true &&
!(r.travel && typeof r.travel === "object" && object(r.travel).ok === false);
return (
<li key={i}>
<strong>{text(r.name)}</strong>
<Badge tone={complete ? "good" : "warn"}>
{complete
? "已完成"
: r.checkin_ok === true ||
r.claimed === true ||
r.departed === true ||
r.buddy_claimed === true ||
r.agreement_accepted === true ||
(r.travel &&
typeof r.travel === "object" &&
(object(r.travel).claimed === true ||
object(r.travel).departed === true))
? "部分完成"
: r.skipped
? "已跳过"
: "未完成"}
</Badge>
<span>{text(r.message)}</span>
<TravelSummary
trip={
r.travel && typeof r.travel === "object"
? object(r.travel)
: r.action === "travel" || r.action === "travel-status"
? r
: null
}
/>
</li>
);
})}
</ul>
</Panel>
)}
{!detail && maintenance.length > 0 && <MaintenanceResults results={maintenance} />}
<Panel
title="账号凭证"
hint={
Expand Down Expand Up @@ -1133,6 +1136,14 @@ export function Credentials() {
title={`凭证详情 · ${liveDetail.name ?? liveDetail.id}`}
onClose={() => setDetail(null)}
>
{notice && (
<p className={s.successNotice} role="status">
{notice}
</p>
)}
{busy && <p role="status">操作执行中,请勿重复提交…</p>}
<ErrorNotice message={error} />
{maintenance.length > 0 && <MaintenanceResults results={maintenance} />}
<div className={s.detailBar}>
<div className={s.credCardBadges}>
{renderHealthBadge(liveDetail)}
Expand Down
Loading
Loading