Skip to content

fix(control-center): restore /report/status + /llms to public; add narrow /report/public-stats - #77

Merged
man4ish merged 1 commit into
mainfrom
fix/restore-public-report-stats-and-llms
Sep 2, 2026
Merged

man4ish merged 1 commit into
mainfrom
fix/restore-public-report-stats-and-llms

Conversation

@man4ish

@man4ish man4ish commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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/status and /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. 8705cbf touched no frontend and had no regression test covering the anonymous app.

What's restored to public (revert of an accidental over-gate)

route how it was gated by 8705cbf consumer
GET /report/status Depends(require_permission("platform.manage_infra")) added to the function PublicEcosystemPage.tsx polls it to know when a report exists
GET /llms blanket platform.manage_infra dep on llm_router at include time LlmPage.tsx (ControlApp's anonymous LLMs page)

GET /knowledge-base (the other route on llm_router — returns absolute internal filesystem paths, not part of the public surface) keeps its platform.manage_infra gate, now applied per-route in routes_llm.py (same pattern routes_cron.py already 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 on app (not on report_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
}
  • Explicit-allowlist construction (_PUBLIC_STATS_FIELDS + _build_public_stats() build 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 * Σ(stmts − missed) / Σ(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, not 404: HTTP 200 with an all-null shape (repos_measured 0) when no report exists yet or report_data.json is malformed — same posture GET /dashboard/summary uses 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/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. TestOverGateRegressionGuard + TestPlatformManageInfraAuth assert this.

Tests

  • test_main.py: TestReportPublicStats (exact-keys; fixture-value math; the critical negative test — "never leaks per-repo arrays even when report_data.json is 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 (was test_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.py at 100%.

Verified end-to-end against a locally-run uvicorn:

  • /report/public-stats, /report/status, /llms200 with no Authorization header (public-stats with the exact 5-key shape, clean of repo names / arrays even with a populated report_data.json)
  • /, /report/data, /storage, /knowledge-base, /coverage/status, /cron/jobs401 with no header
  • /knowledge-base, /report/data200 with a platform.manage_infra token

Scope

No frontend changes. No nginx changes — 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.

🤖 Generated with Claude Code

…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
@man4ish
man4ish merged commit bee1cc2 into main Sep 2, 2026
2 checks passed
@man4ish
man4ish deleted the fix/restore-public-report-stats-and-llms branch September 2, 2026 23:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant