feat(stats): add admin-only instance stats endpoints

Closes #346 (closed)

ℹ️ This MR was created with AI assistance. I've used Claude Opus 4.8 for this and ran the Code Review high effort mode.

Why

A Panoramax instance admin currently has no supported way to read operational and growth numbers about their own instance (registered accounts, pictures by processing status, job-queue backlog, ingest freshness, moderation backlog). The only path today is direct SQL against internal tables, which needs DB credentials and a read-only role, couples any consumer to the internal schema (a migration silently breaks it), and bypasses the API's auth model.

This blocks building a Prometheus exporter (or any monitoring/dashboard tool) on a stable contract. As discussed in #346 (closed), this MR exposes that data as authenticated JSON so an exporter can be a separate, community-maintained project — no monitoring dependency is pulled into core.

What this adds

Two admin-only JSON endpoints (new geovisio/web/stats.py blueprint):

GET /api/admin/stats

  • accounts: total, and new accounts per fixed window (1d / 7d / 30d)
  • pictures: total, breakdown by processing status, new-pictures per window, and seconds since the most recent insert (ingest freshness; null when empty)
  • sequences: total, breakdown by status and by visibility
  • jobs: pending count per task, and age of the oldest job currently due (null when none)

GET /api/admin/reports/stats (privacy-safe)

  • report counts grouped by status, by status+issue, and an unresolved total
  • counts and grouping keys only — no report bodies, reporter email, or reporter identity

Design

  • Auth reuses the existing reports gate@auth.login_required() + account.can_check_reports(), so both bearer token and cookie work. No new auth machinery. Non-admin → 403, unauthenticated → 401.
  • Aggregate, low-cardinality only. Every field is an instance-wide aggregate or a breakdown over a bounded enum (status, visibility, task, issue). No per-account, per-picture or per sequence identifier appears, so consumers can't bake PII or high-cardinality series into a TSDB.
  • Stable series. Enum breakdowns are built with enum_range LEFT JOINs, so every bounded key is always present (0 when absent) and picks up new enum values automatically. Nullable sequences.visibility gets an explicit unset bucket, so the breakdown always sums to total.
  • Read-only aggregate SQL, no schema changes.
  • JSON field names are treated as a stable, additive-only contract.

Notes for reviewers

  • The bulk of the diff is one self-contained new module plus its tests. The only edits to existing files are the blueprint registration in geovisio/__init__.py, two schema entries in geovisio/web/docs.py, and a CHANGELOG line.
  • docs/api/open_api.json is regenerated (make generate-swagger), not hand-edited.
  • There is some overlap with the existing beta GET /api/jobs/stats: this endpoint adds the oldest-due-job age and puts the numbers behind the admin gate. Happy to consolidate if you'd prefer.
  • The Prometheus exporter stays a separate community project and is not part of this MR.

Out of scope (possible later steps that reuse this contract)

  • A native Prometheus /metrics on the instance
  • Federating instance stats into the meta-catalog

Tests

tests/test_web_stats.py:

  • auth: admin 200, non-admin 403, unauthenticated 401 for both endpoints
  • data: seeded accounts / pictures / sequences / jobs / reports, each aggregate asserted
  • edge cases: empty job queue, no pictures (freshness), no reports, due vs delayed jobs, NULL sequence visibility → unset
  • privacy: report-count responses contain no bodies or reporter identity

Merge request reports

Loading