fix(control-center): restore /report/status + /llms to public; add narrow /report/public-stats - #77
Merged
Conversation
…rrow /report/public-stats Follows the 2026-09-02 investigation into how commit 8705cbf ("gate unauthenticated routes") interacted with the Public Read-Only Control Center design (91755fb / 3cbc785, docs/public-control-center.md). That design established a deliberate two-tier split: control.omnibioai.org serves an anonymous, no-login React dashboard (VITE_APP_MODE=control, ControlApp.tsx) whose pages call a specific set of intentionally-public backend routes; admin.omnibioai.org keeps the authenticated surface. 8705cbf's route audit was mostly correct but applied a uniform "gate everything unauthenticated" sweep that collapsed two of those deliberately-public routes into the admin gate, breaking the anonymous dashboard it was auditing. WHAT'S RESTORED TO PUBLIC (revert of an accidental 8705cbf over-gate) - GET /report/status -- backs PublicEcosystemPage.tsx (polls it to know when a report exists). Was a bare @app.get; 8705cbf added a platform.manage_infra Depends to the function. Removed. Restored to its exact pre-8705cbf behavior/response shape. - GET /llms -- backs ControlApp's anonymous LLMs page (LlmPage.tsx). 8705cbf gated the whole llm_router at include time. Removed the router-level dependency; GET /knowledge-base (the other route on that router -- returns absolute internal filesystem paths, NOT part of the public surface) keeps the same platform.manage_infra gate, now applied per-route in routes_llm.py (the pattern routes_cron.py already uses for its two gated GET routes). Both routes are named as intentionally public in ControlApp.tsx's own doc comment (llm_router / report_router listed as "no permission dependency in main.py"). GET /report itself (the bare redirect to /) was NOT restored -- it only 302s to the correctly-gated / and no ControlApp page calls it; left gated, noted for a possible follow-up. WHAT'S NEWLY ADDED - GET /report/public-stats -- new, deliberately unauthenticated, registered directly on `app` (not on report_router). Returns exactly five ecosystem-wide aggregate scalars and nothing else: generated_at, total_lines, total_files, ecosystem_coverage_percent, repos_measured Built by explicit allowlist (_PUBLIC_STATS_FIELDS + _build_public_stats construct a fresh dict and project through the allowlist), mirroring routes_dashboard.py's PUBLIC_FIELDS / _apply_public_contract posture: report_data.json is never spread/filtered into the response, so projects[], languages[], the per-repo coverage[] rows, and gitStatus[] cannot appear under any code path, and a sensitive field added to report_data.json later cannot leak by omission. ecosystem_coverage_percent is a STATEMENT-WEIGHTED average (100 * sum(stmts-missed) / sum(stmts) over rows with valid data), deliberately distinct from the HTML report's unweighted mean(pct) -- documented in-code so the two aren't "reconciled". Fails to null (HTTP 200, all-null shape, repos_measured 0) when no report exists yet or report_data.json is malformed -- never 404, same posture GET /dashboard/summary uses for an anonymous caller. Rationale for a new endpoint rather than reverting GET /report/data: that route's per-repo arrays leak the private repo roster and live dev state (branch names, unpushed counts). Reverting it was explicitly declined; this narrow endpoint is the agreed replacement for the one genuinely-public slice of that data. GET /report/data stays platform.manage_infra-gated, untouched. WHAT STAYS GATED (unchanged from 8705cbf -- regression-guarded) GET /, GET /report, GET /report/data, GET /coverage/status, GET /knowledge-base, GET /storage, GET /cron/jobs, GET /cron/jobs/{id}/log -- all still 401 without a token. TESTS - test_main.py: TestReportPublicStats (exact-keys, fixture-value math, the critical negative "never leaks per-repo arrays even when report_data.json is fully populated" test, token-doesn't-widen-shape, null-shape-not-404, malformed-data, defensive-branch coverage); TestLlmsPublicAccess; TestOverGateRegressionGuard (the named still-gated list); TestReportStatus.test_200_when_no_token (was test_401_when_no_token). TestPlatformManageInfraAuth._cases() drops /report/status and /llms, keeps everything else. - test_public_dashboard_no_leak.py: forbidden-key sweep extended to /report/public-stats, plus an explicit per-repo-array/repo-name check. - test_routes_llm.py: comment updated to the new exposure model (client token now harmless for /llms, still required for /knowledge-base). Full backend suite: 1716 passed, 98.47% coverage (gate 98%; baseline 1700 / 98.46% from 8705cbf). main.py at 100%. Verified end-to-end against a locally-run uvicorn: /report/public-stats and /report/status and /llms return 200 with no Authorization header (public-stats with the exact 5-key shape, and clean of repo names / arrays even with a populated report_data.json); /, /report/data, /storage, /knowledge-base, /coverage/status, /cron/jobs return 401 with no header; /knowledge-base and /report/data return 200 with a platform.manage_infra token. No frontend changes. No changes to nginx config (control-center-web's api-proxy.conf already proxies /report/* and /llms to the backend with no auth logic of its own; the backend gate is authoritative). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F6iBGu7q5k1n2KtMZ5ZE5P
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Follows the 2026-09-02 investigation into how commit
8705cbf("gate unauthenticated routes") interacted with the Public Read-Only Control Center design (91755fb/3cbc785,docs/public-control-center.md).That design is a deliberate two-tier split:
control.omnibioai.org— anonymous, no-login React dashboard (VITE_APP_MODE=control,ControlApp.tsx), whose pages call a specific set of intentionally-public backend routes.admin.omnibioai.org— the authenticated admin surface (AdminApp.tsx,AuthGate).8705cbf's route audit was mostly correct, but applied a uniform "gate everything currently unauthenticated" sweep. It gated/(full HTML report — correct),/report/data,/coverage/status,/cron/jobs*,/knowledge-base,/storage(all correct/defensible), and also/report/statusand/llms— two routes the public dashboard is built on. Result:control.omnibioai.org's Ecosystem Report and LLMs pages have been returning 401 to the anonymous visitors they were designed for.8705cbftouched no frontend and had no regression test covering the anonymous app.What's restored to public (revert of an accidental over-gate)
8705cbfGET /report/statusDepends(require_permission("platform.manage_infra"))added to the functionPublicEcosystemPage.tsxpolls it to know when a report existsGET /llmsplatform.manage_infradep onllm_routerat include timeLlmPage.tsx(ControlApp's anonymous LLMs page)GET /knowledge-base(the other route onllm_router— returns absolute internal filesystem paths, not part of the public surface) keeps itsplatform.manage_infragate, now applied per-route inroutes_llm.py(same patternroutes_cron.pyalready uses for its two gated GET routes).Both restored routes are named as intentionally public in
ControlApp.tsx's own doc comment.GET /report(the bare redirect to/) was not restored — it only 302s to the correctly-gated/and no ControlApp page calls it; left gated, flagged as a possible follow-up.What's newly added
GET /report/public-stats— new, deliberately unauthenticated, registered directly onapp(not onreport_router). Returns exactly five ecosystem-wide aggregate scalars and nothing else:{ "generated_at": "2026-09-02T04:00:00+00:00", "total_lines": 1863200, "total_files": 14820, "ecosystem_coverage_percent": 82.0, "repos_measured": 22 }_PUBLIC_STATS_FIELDS+_build_public_stats()build a fresh dict and project through the allowlist), mirroringroutes_dashboard.py'sPUBLIC_FIELDS/_apply_public_contract()posture.report_data.jsonis never spread/filtered into the response — soprojects[],languages[], the per-repocoverage[]rows, andgitStatus[]cannot appear under any code path, and a sensitive field added toreport_data.jsonlater cannot leak by omission.ecosystem_coverage_percentis a statement-weighted average (100 * Σ(stmts − missed) / Σ(stmts)over rows with valid data), deliberately distinct from the HTML report's unweightedmean(pct)— documented in-code so the two aren't "reconciled".repos_measured0) when no report exists yet orreport_data.jsonis malformed — same postureGET /dashboard/summaryuses for an anonymous caller.Why a new endpoint rather than reverting
GET /report/data: that route's per-repo arrays leak the private repo roster and live dev state (branch names, unpushed-commit counts). Reverting it was explicitly declined. This narrow endpoint is the agreed replacement for the one genuinely-public slice of that data.GET /report/datastaysplatform.manage_infra-gated, untouched.What stays gated (unchanged from
8705cbf— regression-guarded)GET /·GET /report·GET /report/data·GET /coverage/status·GET /knowledge-base·GET /storage·GET /cron/jobs·GET /cron/jobs/{id}/log— all still 401 without a token.TestOverGateRegressionGuard+TestPlatformManageInfraAuthassert this.Tests
test_main.py:TestReportPublicStats(exact-keys; fixture-value math; the critical negative test — "never leaks per-repo arrays even whenreport_data.jsonis fully populated with them", by key and by value; token-doesn't-widen-shape; null-shape-not-404; malformed-data; defensive branches).TestLlmsPublicAccess.TestOverGateRegressionGuard.TestReportStatus.test_200_when_no_token(wastest_401_when_no_token).TestPlatformManageInfraAuth._cases()drops/report/status+/llms, keeps everything else.test_public_dashboard_no_leak.py: forbidden-key sweep extended to/report/public-stats+ an explicit per-repo-array / repo-name check.test_routes_llm.py: comment updated to the new exposure model.Full backend suite: 1716 passed, 98.47% coverage (gate 98%; baseline 1700 / 98.46% from
8705cbf).main.pyat 100%.Verified end-to-end against a locally-run
uvicorn:/report/public-stats,/report/status,/llms→ 200 with noAuthorizationheader (public-stats with the exact 5-key shape, clean of repo names / arrays even with a populatedreport_data.json)/,/report/data,/storage,/knowledge-base,/coverage/status,/cron/jobs→ 401 with no header/knowledge-base,/report/data→ 200 with aplatform.manage_infratokenScope
No frontend changes. No nginx changes —
control-center-web'sapi-proxy.confalready proxies/report/*and/llmsto the backend with no auth logic of its own; the backend gate is authoritative.🤖 Generated with Claude Code