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

@@ -34,6 +34,7 @@ import logging
import os
import sys
import aiohttp
from aiohttp import web
_pkg_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
@@ -93,6 +94,41 @@ async def _pm2_run(*args: str, timeout: float = 10) -> tuple[int, bytes, bytes]:
return proc.returncode or 0, out, err
# claude.ai subscription usage — the 5-hour / weekly utilization % the Claude
# Code status bar shows, from the (undocumented) OAuth usage endpoint. Host-only:
# the OAuth token lives in the CLI credentials file on the host, never in the
# container. Read-only (no auth), like /pm2. The claude-code User-Agent is
# REQUIRED — without it the request lands in an aggressively rate-limited bucket.
_CLAUDE_CRED_PATH = "/home/chaim/.claude/.credentials.json"
_OAUTH_USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
_USAGE_UA = "claude-code/2.1.177"
async def usage_status(request: web.Request) -> web.Response:
"""Proxy the claude.ai subscription usage % (host-only — needs the local
OAuth token). Returns the endpoint's JSON, or a 502 with an error string."""
try:
with open(_CLAUDE_CRED_PATH) as f:
token = json.load(f)["claudeAiOauth"]["accessToken"]
except Exception as e:
return web.json_response({"error": f"no claude credentials: {e}"}, status=502)
headers = {
"Authorization": f"Bearer {token}",
"User-Agent": _USAGE_UA,
"anthropic-beta": "oauth-2025-04-20",
}
try:
timeout = aiohttp.ClientTimeout(total=15)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(_OAUTH_USAGE_URL, headers=headers) as r:
if r.status != 200:
return web.json_response(
{"error": f"usage endpoint {r.status}"}, status=502)
return web.json_response(await r.json())
except Exception as e: # never throw
return web.json_response({"error": f"usage fetch failed: {e}"}, status=502)
async def pm2_status(request: web.Request) -> web.Response:
"""Return a trimmed ``pm2 jlist`` for the legal-ai background services."""
try:
@@ -235,6 +271,7 @@ def build_app() -> web.Application:
app = web.Application(client_max_size=64 * 1024 * 1024)
app.router.add_get("/health", health)
app.router.add_get("/pm2", pm2_status)
app.router.add_get("/usage", usage_status)
app.router.add_post("/pm2/control", pm2_control)
app.router.add_post("/fetch", fetch)
return app