Phase 6a — Backbone admin/ops + ingest¶
Status: Closed 2026-06-09 (split from the original Phase 6 per mid-review-phase-6.md Option 2). The 6 backbone/ops items below shipped and merged; the 8 UI-bearing / cross-stack items moved to Phase 6b — Admin panel UX. Shipped (6a): 6.6 async ingest (#293), 6.7 SSRF + admin download (#282), 6.9 result cache (#283), 6.11 usage metering (#284), 6.13 release pipeline (#281), 6.14 secrets ADR (#102). Moved to 6b: 6.1, 6.2, 6.3, 6.4, 6.5, 6.8, 6.10, 6.12 — see phase-6b-admin-panel.md. Predecessor: Phase 5 — UX quality. Successor: Phase 6b — Admin panel UX.
The scope sections below are the original Phase 6 specification, retained as the authoritative acceptance criteria for both the shipped 6a items and the 6b items (whose issues copy from here).
Goal¶
Make the Phase 2 multi-tenancy + RBAC plumbing visible and operable: admin panel, dataset catalog (user-facing + admin-curated), async ingest pipeline (demoted to Tier 3 per R4 B1, but still required to scale real datasets), external dataset polling, in-app notifications for async work, usage metering for observability, deployment/release pipeline ADR, audit log UI, and the predefined-results cache. This is the phase where a self-hoster can actually run the platform in a multi-user mode without shell access.
Entry conditions¶
- Phase 5 closed: error taxonomy retrofit live, i18n scaffold in place, settings page exists (provides the surface admin toggles will attach to).
- ADR-029 (DO Spaces layout) merged in Phase 3.
Scenariomodel + provenance in place from Phase 3.
Out of scope (deferred)¶
- Managed hosting, K8s, ArgoCD, cert-manager — parked in phase-managed-hosting.md.
- Stripe metering + billing — Tier 4.
- Full scenario versioning (#P) — Tier 3, later phase.
- Reports (PDF, templates) — Phase 7.
- CLIMADA compute adapter, IngestAdapter ABC — Phase 7.
Scope¶
6.1 — Admin panel shell (#21)¶
Context. Vision #21 Tier 2. Dedicated /admin/* route tree behind a
hard RBAC gate (requires admin role from Phase 2). Lazy-loaded bundle so
regular users don't pay the download cost. Top-level tabs: Users, Orgs,
Datasets, Jobs, Audit, System, Branding.
Acceptance criteria.
- [ ] New route tree in climate-lama-ui/src/admin/, lazy-imported from
the main router.
- [ ] Route guard rejects non-admin users with a 403 page; API also
enforces per endpoint (defence in depth).
- [ ] Tab shell with stubs for each section — subsequent work items fill
the panes.
- [ ] Separate bundle-split confirmed (Vite output shows admin chunk only
loads when visited).
- [ ] Vitest: guard blocks non-admin; admin can navigate all tabs.
Cross-stack: UI + backbone (admin-scoped endpoints already mostly
exist; this item audits coverage).
Linked ADR: none.
Suggested /analyze tier: 3 (Sonnet · high).
6.2 — Users + orgs management (admin panel)¶
Context. Vision #21 Tier 2 flesh-out. Admin can list/search users,
invite new ones, assign roles, deactivate, reset API keys. For orgs:
list, create, edit metadata (billing_email, plan_tier, data_source_allowlist),
delete (soft).
Acceptance criteria.
- [ ] Backbone endpoints: GET/POST/PATCH/DELETE /v1/admin/users,
same for /v1/admin/orgs. RBAC: admin role required; platform-admin
flag for cross-org operations (introduce is_platform_admin boolean on
users if not already there).
- [ ] Invite flow: admin creates user → email sent with signup link
(piggybacks on Phase 2 email verification if present; otherwise prints
link in dev mode).
- [ ] UI panes: searchable user list, user detail with role edit +
deactivate, org list with editable metadata.
- [ ] Audit every write to the audit log (6.4 below).
- [ ] Tests cover: role change, deactivate, org metadata edit, platform-
admin gate on cross-org calls.
Cross-stack: backbone (endpoints + schema tweaks) + UI.
Linked ADR: none.
Suggested /analyze tier: 4 (Opus · high) — RBAC-adjacent, schema
change, two repos.
6.3 — Datasets surface (#6 + #7)¶
Context. Vision #6 + #7 Tier 2. Unified "Datasets" UI with Catalog + Upload tabs. Exposures and hazards share the surface with hazard-specific filters. Drives both the user-facing browse (this item) and the admin- only curation (6.5).
Acceptance criteria.
- [ ] New /datasets route with tabs "Hazards" + "Exposures", each with
Catalog (shared + global) + Upload panes.
- [ ] Backbone GET /v1/hazards and GET /v1/exposures support
source, license, hazard_type, region, scenario, year_range
query filters.
- [ ] Each row shows name, source, license, date, type, preview action.
- [ ] Preview endpoint GET /v1/hazards/{id}/preview and the exposure
equivalent return first N rows + bbox + sample thumbnail URL (for
rasters) or a point-density summary (for vectors).
- [ ] Upload: CSV + XLSX (CLIMADA entity format) for exposures; existing
HazardUploadModal.tsx extended with metadata fields and manifest-
driven flow per ADR-029.
- [ ] HDF5 + GeoTIFF uploads deferred; show a "Contact admin" link.
Cross-stack: backbone (filters + preview + uploads) + UI (route +
components).
Linked ADR: references ADR-029.
Suggested /analyze tier: 4 (Opus · high) — two endpoints,
filter/preview logic, two upload paths, new route.
6.4 — Audit log (write side + UI) (#E)¶
Context. Vision #E. Write side shipped in this phase (not Phase 3) now that admin-panel endpoints are the primary audit source. UI browsing lives in admin panel Audit tab.
Acceptance criteria.
- [ ] Alembic migration: audit_log(id UUID, org_id FK, user_id FK,
action TEXT, target_type TEXT, target_id UUID, metadata JSONB,
created_at TIMESTAMP) with indexes on (org_id, created_at) and
(target_type, target_id).
- [ ] FastAPI middleware / dependency writes audit entries on every
admin-panel write endpoint (6.2) and on every user-impactful action
(scenario delete, API key rotation). Append-only — no updates, no
deletes.
- [ ] Admin UI Audit tab: paginated table, filter by user / action /
target-type / date range; JSON detail modal per entry.
- [ ] RBAC: admin sees own org; platform-admin can filter cross-org.
- [ ] Tests: every admin endpoint writes exactly one audit row.
Cross-stack: backbone (schema + middleware) + UI (admin panel tab).
Linked ADR: none.
Suggested /analyze tier: 4 (Opus · high) — schema + middleware +
cross-module.
6.5 — Dataset catalog (DO Spaces) — admin curation (#19)¶
Context. Vision #19 Tier 2. 3-dataset curated catalog (JRC flood, CMIP6 TC tracks, Worldpop population) with versioned JSON manifest per ADR-029 layout.
Acceptance criteria.
- [ ] Catalog manifest catalog.v1.json in DO Spaces raw/ root,
listing every dataset: {name, type, source, license, region, year,
scenario, s3_url, sha256, manifest_url}.
- [ ] Backbone service CatalogClient fetches manifest at boot, caches
locally, refreshes on admin-triggered reload.
- [ ] Admin UI Datasets tab (inside 6.1 shell): list catalog entries,
view manifest, trigger refresh, "request ingest" action (hands off to
6.6 pipeline).
- [ ] Each dataset entry ships a manifest.json at its leaf per ADR-029.
- [ ] Self-hosters: env var CATALOG_URL points at their own bucket /
catalog URL; same code path.
- [ ] Seed the 3 catalog entries + their manifests in CI; fixtures under
tests/fixtures/catalog/.
Cross-stack: backbone (CatalogClient + admin endpoints) + UI (admin
tab) + data ops (seed authoring).
Linked ADR: references ADR-029.
Suggested /analyze tier: 4 (Opus · high) — new service + external
I/O + versioning.
6.6 — Async ingest pipeline (#27, demoted)¶
Context. Vision #27 demoted to Tier 3 per R4 B1 but still required to make real datasets usable. Celery chord (parallel subtasks + callback) that consumes a DO-Spaces URI, validates, chunks, and writes to the DB. Stream bytes, never load full files in memory.
Acceptance criteria.
- [ ] worker/ingest/pipeline.py defines the chord pattern:
download → validate → chunk → N parallel writes → aggregate →
commit.
- [ ] Chunk size configurable per hazard type in settings.
- [ ] ingest_jobs table tracks state per chord execution: id,
dataset_id, status, progress_pct, logs TEXT, error_code, started_at,
completed_at.
- [ ] Admin UI: ingest jobs list with progress + logs + retry/cancel
actions.
- [ ] Memory test: 5GB netCDF fixture ingests without exceeding 1GB
worker RSS.
- [ ] Integration test: end-to-end ingest of a small fixture.
Cross-stack: worker + backbone (endpoints + schema) + UI (admin
view).
Linked ADR: none.
Suggested /analyze tier: 5 (Opus · extra high) — chord pattern is
load-bearing; concurrency/memory correctness matters; new subsystem.
6.7 — Admin download service + SSRF hardening (B4)¶
Context. Vision #27 download side + R4 B4. Admin triggers raw dataset downloads from external sources into DO Spaces. Must be resumable, streaming, observable; and SSRF-hardened via a per-org source allowlist + platform default list.
Acceptance criteria.
- [ ] Celery task download_to_spaces(source_url, target_path, org_id)
— streams bytes in chunks, records progress in downloads table, writes
to raw/{source}/... per ADR-029.
- [ ] downloads table: id, org_id, source_url, target_path, status,
bytes_total, bytes_done, retry_count, error_code.
- [ ] SSRF policy: platform default allowlist (CLIMADA data API, JRC,
Copernicus CDS, NOAA, OSM) baked into config; per-org additions need
platform-admin approval via 6.2 admin-user endpoint.
- [ ] URL resolution guarded: resolve DNS, reject private IP ranges
(RFC 1918, 127.0.0.0/8, ::1, link-local), reject redirects leaving
the allowlist.
- [ ] Admin UI: download manager surface with progress, logs, retry,
cancel.
- [ ] Short ADR "SSRF policy for admin download endpoints" capturing the
allowlist rules — goes into DECISIONS.md with **Phase**: 6.
- [ ] Integration test: malicious URL (file://, localhost, private IP)
refused with coded error.
Cross-stack: worker + backbone + UI + docs.
Linked ADR: authors a new ADR (number assigned when merged, e.g.,
ADR-031).
Suggested /analyze tier: 5 (Opus · extra high) — security-sensitive
per /analyze rubric; SSRF is a real attack surface.
6.8 — External dataset polling (#22)¶
Context. Vision #22 Tier 3. Scheduled mechanism watches external dataset publishers for new/updated raw datasets. Notifies admin (via 6.10 notifications) for review before ingestion.
Acceptance criteria.
- [ ] DatasetSource ABC in src/climate_lama/ingest/sources/base.py
with list_available(), get_metadata(id), download(id, dest).
- [ ] Three first-party DatasetSource implementations: JRC European
flood, Copernicus ERA5, Worldpop. Each in its own module.
- [ ] Celery beat schedule runs each source weekly; diff against
dataset_source_catalog table; on change, inserts a row in
pending_ingests with status=awaiting_review.
- [ ] Admin UI pending-ingests tab: review + approve (triggers 6.6
pipeline) + reject.
- [ ] No auto-ingest in v1; human in the loop by design.
- [ ] Unit tests with mocked source responses for each implementation.
Cross-stack: backbone (new module + Celery beat) + UI (admin tab).
Linked ADR: references ADR-027 (adapter-by-design) — DatasetSource
is one of the five extension points.
Suggested /analyze tier: 4 (Opus · high) — three adapters, Celery
beat + review workflow.
6.9 — Predefined results caching (#20)¶
Context. Vision #20 Tier 2. Deterministic SHA256 cache key; pre-warm popular scenarios; invalidate on input version bumps.
Acceptance criteria.
- [ ] Cache key: SHA256(exposure_dataset_id || exposure_dataset_sha256
|| hazard_dataset_id || hazard_dataset_sha256 || impact_function_id ||
impact_function_version || year || scenario || discount_rate ||
growth_rate). Measures NOT in key — applied post-cache as multiplier
per vision #20 rationale.
- [ ] Schema: result_cache(cache_key TEXT PK, result_id UUID FK,
created_at, hit_count).
- [ ] POST /v1/compute/impact checks cache → hit returns 200 with
cache_hit: true; miss dispatches Celery as today.
- [ ] Celery beat pre-warm job runs hand-curated list of 20 demo
scenarios overnight. List lives in config/prewarm.yaml.
- [ ] Impact function version bump invalidates dependent entries
(already exists from Phase 3 #3.8).
- [ ] Prometheus counter for cache hit rate.
- [ ] Integration test: compute same request twice → second returns
cache_hit: true.
Cross-stack: backbone (schema + lookup + pre-warm) + infra
(Prometheus metric).
Linked ADR: none.
Suggested /analyze tier: 4 (Opus · high) — cache invalidation + key
design.
6.10 — In-app notifications (#30)¶
Context. Vision #30 Tier 3. In-app notification surface (bell icon + dropdown) for async job completion, dataset availability, admin approvals. Minimum viable: polling, no WebSockets.
Acceptance criteria.
- [ ] Schema: notifications(id, user_id, org_id, type, payload JSONB,
created_at, read_at, dismissed_at).
- [ ] Celery hook emits notifications on job completion (6.6 ingest, 6.7
download, compute impact) and admin approvals (6.8 pending-ingests).
- [ ] GET /v1/users/me/notifications?unread=true endpoint with
pagination.
- [ ] UI bell icon with unread count; dropdown lists recent; click marks
read; dismiss permanently removes.
- [ ] Polling interval 30s by default, configurable in settings (5.7).
- [ ] Taxonomy locked to 5 initial types: job_complete,
dataset_available, system_announcement, admin_approval_needed,
download_ready. Adding a type requires updating the ADR-028 code
registry.
Cross-stack: backbone + UI.
Linked ADR: references ADR-028 (codes for notification types).
Suggested /analyze tier: 3 (Sonnet · high).
6.11 — Usage metering observability slice (#M)¶
Context. Vision #M Tier 3 observability slice (Stripe integration stays Tier 4). Instrument compute-seconds / runs / storage per tenant per billing period for admin visibility — no billing wiring.
Acceptance criteria.
- [ ] Every compute job records compute_seconds, cpu_seconds,
result_bytes to a usage_events table.
- [ ] Aggregation view usage_by_period(org_id, period, compute_seconds,
run_count, result_bytes) — monthly roll-up; period = first of month.
- [ ] GET /v1/admin/usage?org_id=...&period=... endpoint.
- [ ] Admin UI: usage tab with bar chart per month, CSV export.
- [ ] Prometheus counter for compute-seconds labelled by org.
- [ ] Schema designed so Stripe metered-billing API can read from it
later without migration (Tier 4 plug-in).
Cross-stack: backbone + UI.
Linked ADR: none.
Suggested /analyze tier: 3 (Sonnet · high).
6.12 — Adaptation measures catalog UX (#10)¶
Context. Vision #10 catalog + selection. Phase 1 shipped the data
model + CRUD API (#54-57); UI is still minimal (CostBenefitPanel.tsx).
This item delivers the Measures page + scenario-level multi-select.
Acceptance criteria.
- [ ] /measures route lists all predefined measures, filterable by
hazard / region / source. Detail view shows cost, effectiveness curve,
source, lifetime.
- [ ] Scenario wizard gains a "select measures" step with multi-select
chips per riskwise-v2 pattern.
- [ ] Cost-benefit panel updated to render the selected measure set; the
compute path already accepts measures from Phase 1.
- [ ] Attribution (#3.10) exposed per measure.
- [ ] Custom measure builder + save-as-set explicitly deferred (vision
#10 Tier 3 later-in).
Cross-stack: UI + (minor) backbone filters on existing /v1/measures
endpoint.
Linked ADR: none.
Suggested /analyze tier: 3 (Sonnet · high).
6.13 — Deployment ADR + release pipeline (#29)¶
Context. Vision #29 Tier 3. Write the ADR (K8s + ArgoCD + Helm stays Tier 4, only the path is documented) and ship the release pipeline: versioned image tags on git tags. Cheap now, pays off for both self- hosters and future managed hosting.
Acceptance criteria.
- [ ] ADR drafted: self-hosters on Docker Compose indefinitely; managed
hosting path sketched (K8s + Helm + ArgoCD + cert-manager) but not
implemented; Docker images treated as the universal artifact.
- [ ] .github/workflows/release.yml: on git tag v*, build backbone
+ UI Docker images, tag with the semver, push to GHCR.
- [ ] Image tags follow v{major}.{minor}.{patch} + latest for the
most recent release.
- [ ] docker-compose.yml references ${CLIMATE_LAMA_TAG:-latest} so
users can pin.
- [ ] Docs-site page deployment/upgrading.md documents the tag
cadence and upgrade path (Alembic migration flow).
- [ ] **Phase**: 6 header line on the ADR.
Cross-stack: docs + CI.
Linked ADR: authors a new ADR (e.g., ADR-032).
Suggested /analyze tier: 3 (Sonnet · high).
6.14 — Secrets and config management ADR (#102)¶
Context. Phase 0 retrospective flagged that no ADR covers how secrets and config are managed across local dev, CI, and (future) hosted deploys. Pulled forward from the parked managed-hosting phase to land alongside the deployment ADR (§6.13) — both are ops decisions self-hosters need documented before any hosted deploy, and pairing them avoids re-doing the discussion when managed hosting revives.
Acceptance criteria.
- [ ] ADR added to docs/DECISIONS.md covering: dev (.env + pydantic-settings), CI (GitHub Actions secrets → env vars), hosted path (per-environment secrets backend candidates — Hetzner vault, Doppler, 1Password Connect, AWS Secrets Manager — decision deferred until managed hosting unparks, alternatives captured).
- [ ] Rotation story documented (who rotates, on what cadence, how rollouts pick up the new value).
- [ ] Naming conventions for env vars codified (prefix, casing, scope markers).
- [ ] Alternatives section covers committed-encrypted-secrets (sops, git-crypt) and explains why the env-var path was chosen instead.
- [ ] **Phase**: 6 header line on the ADR.
- [ ] Cross-referenced from phase-managed-hosting.md retrospective backlog table so the parked phase points at the resolved decision.
Cross-stack: backbone docs only. No code change.
Linked ADR: authors a new ADR (e.g., ADR-033).
Suggested /analyze tier: 2 (Sonnet · medium) — pure prose, existing template.
ADRs authored in this phase¶
| ID | Title | Owner work item |
|---|---|---|
| ADR-031 (approx.) | SSRF policy for admin download endpoints | 6.7 |
| ADR-032 (approx.) | Deployment ADR — Compose, images, K8s path | 6.13 |
| ADR-034 | Secrets and config management strategy | 6.14 |
Numbers assigned at merge time; may shift if earlier phases author additional ADRs.
Exit criteria¶
- All 14 work items closed.
- Admin panel usable for a self-hoster to provision users, orgs, datasets, ingest jobs, audit log, usage views — no shell access required for common ops.
- Admin-panel bundle confirmed lazy-loaded (non-admin users don't download it).
result_cachehit rate visible on the Grafana dashboard.- 3-dataset catalog seeded and browsable in the Datasets surface.
- One full download → ingest → appear-in-catalog round-trip demonstrated end-to-end.
- Release pipeline publishes versioned images on tag.
Risks¶
- 6.6 ingest pipeline is the phase's most complex item. Memory
bounds + partial-failure recovery are the usual traps. Run
/build --reviewnot auto-merge. - 6.7 SSRF is security-sensitive. Independent review required before merge; even a small gap in the allowlist validation can expose internal infra.
- 6.8 polling depends on external services staying shape-stable. Expect adapter breakage when JRC / Copernicus change endpoints; budget maintenance.
- 6.1 admin panel bundle-split: verify with production build output, not just dev mode. Dev mode serves everything.
- 6.11 metering schema is the future billing foundation. Any design change after launch means a migration. Get it right up front.