feat(operations): show real claude.ai subscription usage % on /operations
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s

Surfaces the 5-hour / weekly / weekly-Opus utilization the Claude Code status
bar shows — the authoritative number, not a token estimate. Design approved via
the Claude Design gate (card 02c-operations-usage.html).

Three layers:
- court-fetch-service (host bridge): new GET /usage reads the OAuth token from
  ~/.claude/.credentials.json and proxies /api/oauth/usage with the required
  claude-code User-Agent. Read-only, no auth (like /pm2). Host-only — the token
  never enters the container.
- web/app.py: _ops_subscription_usage() proxies the bridge /usage; the
  /api/operations snapshot gains a `subscription_usage` field (null when the
  undocumented endpoint is unreachable).
- web-ui: SubscriptionUsagePanel renders three meters (label · % · bar · reset)
  at the top of /operations; bar turns amber >75% / red >90%; hidden when usage
  is null. Types added to operations.ts (hand-maintained snapshot type).

Also fixes a pre-existing react/no-unescaped-entities lint error in
learning-panel.tsx (escaped a `"` in Hebrew text — renders identically).

tsc --noEmit passes; lint error count 0. (Full next build is blocked only by the
manual-worktree node_modules symlink — the Docker build has real node_modules.)

Invariants: G2 (usage surfaced through the existing host bridge + /api/operations
snapshot — no parallel control path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 10:34:50 +00:00
parent d093319ffd
commit 6f3c3963a4
5 changed files with 131 additions and 1 deletions

View File

@@ -6525,6 +6525,21 @@ async def _ops_pm2_services() -> dict:
return {"services": [], "error": f"לא ניתן להגיע לשירות-המארח: {e}"}
async def _ops_subscription_usage() -> dict | None:
"""Proxy the host court-fetch-service /usage — the claude.ai subscription
utilization % (5-hour / weekly). Host-only (the OAuth token lives on the
host). Returns the endpoint JSON, or None if unavailable (undocumented
endpoint — the dashboard shows nothing when this is None)."""
try:
async with httpx.AsyncClient(timeout=18.0) as client:
r = await client.get(f"{_COURT_FETCH_SERVICE_URL}/usage")
if r.status_code == 200:
return r.json()
except Exception:
pass
return None
async def _ops_pm2_control(name: str, action: str) -> dict:
"""Proxy a mutating pm2 action to the host bridge (Bearer-authenticated)."""
secret = os.environ.get("COURT_FETCH_SHARED_SECRET", "").strip()
@@ -6636,6 +6651,7 @@ async def operations_snapshot():
]
pm2 = await _ops_pm2_services()
subscription_usage = await _ops_subscription_usage()
controls = await db.get_drain_controls()
bursts = await db.get_drain_bursts()
for svc in pm2["services"]:
@@ -6652,6 +6668,7 @@ async def operations_snapshot():
return {
"services": pm2["services"],
"services_error": pm2["error"],
"subscription_usage": subscription_usage,
"pipelines": {
"court_fetch": {
**_norm_pipeline(