feat(operations): show real claude.ai subscription usage % on /operations
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
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:
@@ -33,12 +33,68 @@ import {
|
||||
type OperationsSnapshot,
|
||||
type PipelineStats,
|
||||
type AgentRun,
|
||||
type SubscriptionUsage,
|
||||
type UsageWindow,
|
||||
} from "@/lib/api/operations";
|
||||
|
||||
function mb(bytes: number): string {
|
||||
return `${Math.round((bytes || 0) / 1024 / 1024)}MB`;
|
||||
}
|
||||
|
||||
// claude.ai subscription usage — the fuel gauge for every agent/drain.
|
||||
// Bar turns amber > 75% and red > 90%; the halacha drain auto-pauses at 100%
|
||||
// and resumes on its own when a window resets.
|
||||
function usageResetLabel(iso: string | null): string {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
return `איפוס ${d.toLocaleTimeString("he-IL", { hour: "2-digit", minute: "2-digit" })}`;
|
||||
}
|
||||
|
||||
function UsageMeter({ label, w }: { label: string; w?: UsageWindow | null }) {
|
||||
const util = w?.utilization;
|
||||
const active = typeof util === "number";
|
||||
const pct = active ? Math.min(100, Math.max(0, util as number)) : 0;
|
||||
const fill = pct >= 90 ? "bg-danger" : pct >= 75 ? "bg-warn" : "bg-gold";
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-baseline justify-between text-[13px] text-ink-soft mb-1.5">
|
||||
<span>{label}</span>
|
||||
{active ? (
|
||||
<b className="text-navy tabular-nums text-[15px] font-bold">{Math.round(pct)}%</b>
|
||||
) : (
|
||||
<b className="text-ink-muted text-sm font-semibold">—</b>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[7px] overflow-hidden rounded-full bg-rule-soft">
|
||||
<span className={`block h-full rounded-full ${fill}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<div className="mt-1.5 text-[11.5px] text-ink-muted">
|
||||
{active ? usageResetLabel(w?.resets_at ?? null) : "לא פעיל כרגע"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SubscriptionUsagePanel({ usage }: { usage: SubscriptionUsage }) {
|
||||
if (!usage) return null; // undocumented endpoint unreachable — show nothing
|
||||
return (
|
||||
<Card className="border-rule">
|
||||
<CardContent className="px-5 py-4">
|
||||
<div className="grid gap-6 sm:grid-cols-3">
|
||||
<UsageMeter label="חלון 5-שעות" w={usage.five_hour} />
|
||||
<UsageMeter label="שבועי · כל המודלים" w={usage.seven_day} />
|
||||
<UsageMeter label="שבועי · Opus" w={usage.seven_day_opus} />
|
||||
</div>
|
||||
<div className="mt-3 border-t border-rule-soft pt-2.5 text-[11.5px] text-ink-muted">
|
||||
המנוי המשותף שמפעיל את כל הסוכנים והדריינרים. הפס נצבע בענבר מעל 75%
|
||||
ובאדום מעל 90%. דריינר-ההלכות משתהה אוטומטית כשחלון מגיע ל-100% וממשיך
|
||||
לבד כשהוא מתאפס.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// mockup 02: every region opens with a navy section heading (h2, 18px) — the
|
||||
// page reads as a sequence of titled sections rather than a stack of cards.
|
||||
function SectionHeader({ children }: { children: React.ReactNode }) {
|
||||
@@ -777,6 +833,9 @@ export default function OperationsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<SectionHeader>מכסת claude.ai</SectionHeader>
|
||||
<SubscriptionUsagePanel usage={data.subscription_usage} />
|
||||
|
||||
<SectionHeader>סוכנים פעילים</SectionHeader>
|
||||
<LiveAgentsPanel />
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ function ProposalReview({ pairId }: { pairId: string }) {
|
||||
return (
|
||||
<div className="rounded-md bg-gold-wash/30 border border-gold/40 p-3 space-y-3 text-[0.8rem]">
|
||||
<p className="text-ink-muted text-[0.75rem]">
|
||||
הצעת הדיסטילציה (סגנון/שיטה בלבד — INV-LRN5). בחר/י מה לאמץ; ייכתב לערוצים שהכותב צורך (שער-יו"ר).
|
||||
הצעת הדיסטילציה (סגנון/שיטה בלבד — INV-LRN5). בחר/י מה לאמץ; ייכתב לערוצים שהכותב צורך (שער-יו"ר).
|
||||
</p>
|
||||
{data.overall_assessment && (
|
||||
<p className="italic text-ink-muted">{data.overall_assessment}</p>
|
||||
|
||||
@@ -42,9 +42,26 @@ export type PipelineStats = {
|
||||
by_status: Record<string, number>; // raw counts, for the curious
|
||||
};
|
||||
|
||||
/** One claude.ai usage window (5-hour / weekly / weekly-per-model). */
|
||||
export type UsageWindow = {
|
||||
utilization: number | null; // 0-100; null when the window is inactive
|
||||
resets_at: string | null;
|
||||
};
|
||||
|
||||
/** claude.ai subscription usage — the same %s the Claude Code status bar shows,
|
||||
* via the (undocumented) OAuth usage endpoint proxied by the host bridge.
|
||||
* null when the endpoint is unreachable. */
|
||||
export type SubscriptionUsage = {
|
||||
five_hour?: UsageWindow | null;
|
||||
seven_day?: UsageWindow | null;
|
||||
seven_day_opus?: UsageWindow | null;
|
||||
seven_day_sonnet?: UsageWindow | null;
|
||||
} | null;
|
||||
|
||||
export type OperationsSnapshot = {
|
||||
services: OpsService[];
|
||||
services_error: string | null;
|
||||
subscription_usage: SubscriptionUsage;
|
||||
pipelines: {
|
||||
court_fetch: PipelineStats & { recent: CourtFetchJob[] };
|
||||
metadata_extraction: PipelineStats;
|
||||
|
||||
Reference in New Issue
Block a user