feat(stats): add admin-only instance stats endpoints
Closes #346 (closed)
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;nullwhen empty)sequences: total, breakdown by status and by visibilityjobs: pending count per task, and age of the oldest job currently due (nullwhen 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_rangeLEFT JOINs, so every bounded key is always present (0 when absent) and picks up new enum values automatically. Nullablesequences.visibilitygets an explicitunsetbucket, so the breakdown always sums tototal. - 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 ingeovisio/web/docs.py, and a CHANGELOG line. docs/api/open_api.jsonis 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
/metricson 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