feat: show daily key usage in dashboard

This commit is contained in:
2026-07-27 03:39:10 +08:00
parent 69f72c09b5
commit 42b2e63a97
5 changed files with 99 additions and 16 deletions
+77 -5
View File
@@ -19,7 +19,7 @@ from typing import Any
APP_NAME = "shusub2"
FALLBACK_VERSION = "0.2.5"
FALLBACK_VERSION = "0.2.6"
DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url"
DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url"
@@ -483,7 +483,8 @@ def fetch_key_usage_payload(logs_url: str, token: str, timeout: int, limit: int
return {"date": today, "trend": points, "stats": stats}
def normalize_key_rows(payload: dict[str, Any]) -> list[dict[str, Any]]:
def normalize_key_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]:
needle = filter_text.strip().lower()
stats = payload.get("stats") if isinstance(payload.get("stats"), dict) else {}
merged: dict[Any, dict[str, Any]] = {}
for point in payload.get("trend") or []:
@@ -491,6 +492,8 @@ def normalize_key_rows(payload: dict[str, Any]) -> list[dict[str, Any]]:
continue
key_id = as_int(point.get("api_key_id"))
name = str(point.get("key_name") or "").strip() or (f"#{key_id}" if key_id else "-")
if needle and needle not in name.lower():
continue
row = merged.setdefault(key_id or name, {"id": key_id, "name": name, "requests": 0, "tokens": 0, "cost": 0.0})
row["requests"] += as_int(point.get("requests"))
row["tokens"] += as_int(point.get("tokens"))
@@ -1189,12 +1192,13 @@ def run_textual(
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("a", "focus_accounts", "Accounts"),
("k", "focus_keys", "Keys"),
("l", "focus_logs", "Logs"),
("e", "focus_errors", "Errors"),
]
def __init__(self) -> None:
super().__init__()
super().__init__(classes="dashboard")
self.accounts_payload: dict[str, Any] = {}
self.status_payload: dict[str, Any] = {}
self.monitor_error = ""
@@ -1203,10 +1207,14 @@ def run_textual(
self.logs_error = ""
self.errors_payload: dict[str, Any] = {}
self.errors_error = ""
self.keys_payload: dict[str, Any] = {}
self.keys_error = ""
self.account_rows: list[dict[str, Any]] = []
self.key_rows: list[dict[str, Any]] = []
self.log_rows: list[dict[str, Any]] = []
self.error_rows: list[dict[str, Any]] = []
self.account_by_key: dict[str, dict[str, Any]] = {}
self.key_by_key: dict[str, dict[str, Any]] = {}
self.log_by_key: dict[str, dict[str, Any]] = {}
self.error_by_key: dict[str, dict[str, Any]] = {}
self.active_table = "accounts"
@@ -1216,6 +1224,7 @@ def run_textual(
yield Static("", id="summary")
yield Input(placeholder="filter", id="filter")
yield DataTable(id="accounts")
yield DataTable(id="keys")
yield DataTable(id="logs")
yield DataTable(id="errors")
yield Static("", id="detail")
@@ -1242,6 +1251,15 @@ def run_textual(
("Avail", 7),
),
)
self.configure_table(
self.query_one("#keys", DataTable),
(
("KEY", 16),
("Today", 9),
("Tokens", 8),
("Req", 6),
),
)
self.configure_table(
self.query_one("#logs", DataTable),
(
@@ -1280,6 +1298,11 @@ def run_textual(
def action_focus_accounts(self) -> None:
self.focus_table("accounts")
def action_focus_keys(self) -> None:
table = self.query_one("#keys", DataTable)
if table.display:
self.focus_table("keys")
def action_focus_logs(self) -> None:
self.focus_table("logs")
@@ -1300,7 +1323,7 @@ def run_textual(
self.focus_table(self.active_table)
def refresh_all(self, refresh: bool = False) -> None:
self.query_one("#status", Static).update("refreshing accounts, logs, and errors...")
self.query_one("#status", Static).update("refreshing accounts, keys, logs, and errors...")
self.refresh_accounts(refresh=refresh)
self.refresh_logs()
self.refresh_errors()
@@ -1326,8 +1349,22 @@ def run_textual(
except Exception as exc:
self.logs_error = str(exc)
self.render_logs()
self.refresh_keys()
self.render_meta()
def refresh_keys(self) -> None:
self.keys_error = ""
if not str(logs_token or "").strip():
self.keys_payload = {}
self.keys_error = "admin token not configured"
else:
try:
self.keys_payload = fetch_key_usage_payload(logs_url, logs_token, timeout)
except Exception as exc:
self.keys_payload = {}
self.keys_error = str(exc)
self.render_keys()
def refresh_errors(self) -> None:
self.errors_error = ""
if not str(logs_token or "").strip():
@@ -1349,6 +1386,7 @@ def run_textual(
def render_tables(self) -> None:
self.render_accounts()
self.render_keys()
self.render_logs()
self.render_errors()
self.render_meta()
@@ -1374,6 +1412,25 @@ def run_textual(
key=key,
)
def render_keys(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.key_rows = normalize_key_rows(self.keys_payload, filter_text)
table = self.query_one("#keys", DataTable)
table.clear()
self.key_by_key = {}
for index, row in enumerate(self.key_rows):
key = f"key-{row['id']}-{index}"
self.key_by_key[key] = row
color = key_color(row["name"])
table.add_row(
Text(str(row["name"]), style=color) if color else str(row["name"]),
format_cost(row["cost"]),
format_count(row["tokens"]),
format_count(row["requests"]),
key=key,
)
table.display = bool(self.key_rows) and self.app.size.height >= 20
def render_logs(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.log_rows = normalize_log_rows(self.logs_payload, filter_text)
@@ -1430,9 +1487,14 @@ def run_textual(
logs_total = as_int(logs_envelope(self.logs_payload).get("total"))
if not logs_total:
logs_total = len(log_items(self.logs_payload))
key_summary = "keys -"
if self.key_rows:
top_key = self.key_rows[0]
key_summary = f"keys {top_key['name']} {format_cost(top_key['cost'])}"
summary = (
f"accounts {len(self.account_rows)}/{account_total} ({usable} usable, "
f"{format_cost(totals.get('today_cost_usd'))}) | "
f"keys {len(self.key_rows)} ({key_summary.removeprefix('keys ')}) | "
f"logs {len(self.log_rows)}/{logs_total} | "
f"errors {len(self.error_rows)}/{self.errors_total()}"
)
@@ -1456,6 +1518,8 @@ def run_textual(
status_bits.append(f"logs: {self.logs_error}")
if self.errors_error:
status_bits.append(f"errors: {self.errors_error}")
if self.keys_error and self.keys_error != self.logs_error:
status_bits.append(f"keys: {self.keys_error}")
if not self.errors_error and self.errors_payload:
sources = self.errors_payload.get("sources") if isinstance(self.errors_payload.get("sources"), dict) else {}
source_bits = []
@@ -1470,6 +1534,7 @@ def run_textual(
def render_active_detail(self) -> None:
rows_by_table = {
"accounts": self.account_rows,
"keys": self.key_rows,
"logs": self.log_rows,
"errors": self.error_rows,
}
@@ -1490,6 +1555,11 @@ def run_textual(
error = str(row.get("error") or "").strip()
if error:
detail += f" | {error}"
elif table_id == "keys":
detail = (
f"{row['name']} | today {format_cost(row['cost'])} | "
f"{format_count(row['tokens'])} tokens | {format_count(row['requests'])} req"
)
elif table_id == "logs":
detail = log_detail_line(row)
else:
@@ -1498,7 +1568,7 @@ def run_textual(
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
table_id = str(event.control.id or "")
if table_id not in {"accounts", "logs", "errors"}:
if table_id not in {"accounts", "keys", "logs", "errors"}:
return
if event.control.has_focus:
self.active_table = table_id
@@ -1506,6 +1576,7 @@ def run_textual(
return
row_maps = {
"accounts": self.account_by_key,
"keys": self.key_by_key,
"logs": self.log_by_key,
"errors": self.error_by_key,
}
@@ -1892,6 +1963,7 @@ def run_textual(
#filter:focus { border: none; }
#accounts { height: 1fr; min-height: 3; }
#keys { height: auto; max-height: 6; }
.dashboard #keys { height: 1fr; min-height: 0; max-height: 100%; }
#logs { height: 1fr; min-height: 3; }
#errors { height: 1fr; min-height: 3; }
#detail { height: 2; padding: 0 1; background: $surface-lighten-1; }