feat(ops): /operations dashboard — everything running in the background

A single live page for all the background work that downloads/analyses, so the
chair can see what's running instead of guessing.

- court_fetch_service: GET /pm2 (unauthenticated, host-only) → trimmed pm2 jlist
  for the legal-* services (status, restarts, mem, cron schedule).
- FastAPI GET /api/operations: aggregates the DB-backed pipelines (court_fetch
  jobs, metadata + halacha extraction queues, halacha review gate,
  missing_precedents, digests, recent court ingests) and proxies the host /pm2
  over the docker bridge (graceful if the host service is down).
- web-ui /operations page (+ src/lib/api/operations.ts hook, nav entry under
  admin): services grid (with Hebrew labels + schedules) + pipeline cards +
  recent-fetch / recent-ingest lists. Auto-refreshes every 5s.

tsc --noEmit clean; pm2 status carries nothing sensitive and the bind
(10.0.1.1) is host/container-only, so /pm2 needs no secret.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 07:28:41 +00:00
parent 64b9bd9d99
commit 34d80a39e5
5 changed files with 488 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "./client";
export type OpsService = {
name: string;
status: string;
restarts: number;
uptime_ms: number;
cpu: number;
memory_bytes: number;
cron: string;
autorestart: boolean;
};
export type CourtFetchJob = {
case_number_norm: string;
citation_raw: string;
tier: string;
status: string;
error: string;
updated_at: string;
};
export type IngestedRow = {
case_number: string;
court: string;
source_url: string;
created_at: string;
};
export type OperationsSnapshot = {
services: OpsService[];
services_error: string | null;
pipelines: {
court_fetch: { by_status: Record<string, number>; recent: CourtFetchJob[] };
metadata_extraction: { by_status: Record<string, number>; queued: number };
halacha_extraction: { by_status: Record<string, number>; queued: number };
halacha_review: { by_status: Record<string, number> };
missing_precedents: { by_status: Record<string, number> };
digests: { total: number; linked: number };
ingested_recent: IngestedRow[];
};
};
export function useOperations() {
return useQuery({
queryKey: ["operations"],
queryFn: ({ signal }) =>
apiRequest<OperationsSnapshot>("/api/operations", { signal }),
refetchInterval: 5000, // live view of background work
staleTime: 3000,
});
}