From 2603f0e726b83d8529d053a0d351f0525aa3c71b Mon Sep 17 00:00:00 2001 From: david kiania Date: Wed, 10 Jun 2026 12:37:09 +0300 Subject: [PATCH] docs: dashboard_ro role + two-stage rollout; mark Phase 1 done Updates STAGING_FLEETOPS_ARCHITECTURE.md to reflect the dedicated read-only dashboard_ro role (replacing the grafana_ro reuse), the explicit v_trips matview grant, DEFAULT PRIVILEGES, host-only password, and the two-stage plan (staging now, live prod connection later). Notes migrations 17+18 applied; Phase 0 read-only role complete, webhook deploys still pending. Co-Authored-By: Claude Opus 4.8 --- docs/STAGING_FLEETOPS_ARCHITECTURE.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/STAGING_FLEETOPS_ARCHITECTURE.md b/docs/STAGING_FLEETOPS_ARCHITECTURE.md index de9239e..2e9f924 100644 --- a/docs/STAGING_FLEETOPS_ARCHITECTURE.md +++ b/docs/STAGING_FLEETOPS_ARCHITECTURE.md @@ -82,7 +82,7 @@ Traefik host `fleetapi.rahamafresh.com`). Staging mirrors it: | Port | 8890 | **8891** | | Code mount | `~/dashboard_api/` | `~/dashboard_api_staging/` (WIP checkout) | | Deploy script | `~/deploy_dashboard_api.sh` | **`deploy_dashboard_api_staging.sh`** (checked into this repo) | -| DB role | app role (read/write) | **read-only** (`dashboard_ro` / `grafana_ro`) | +| DB role | app role (read/write) | **read-only** `dashboard_ro` (dedicated) | | `v_trips` refresher | **owns it** | **disabled** | | CORS origins | `fleetnow.rahamafresh.com`, `fleetintelligence.…`, `liveposition.…`, **+ `fleetops.rahamafresh.com`** | `fleetnow.fivetitude.com`, `fleetops.fivetitude.com` | @@ -105,9 +105,9 @@ the JSONB/GeoJSON return style of the existing `/webhook/*` routes: | `GET /analytics/filters` | `reporting.v_filter_*` (alias of `GET /webhook/fleet-dashboard`) | Aggregations that aren't thin wrappers get a **new numbered migration** — never edit an applied -one. The fuel roll-up ships as `migrations/17_fleetops_fuel_view.sql` (the live migration head is -**16**, not 13 as older docs imply); it also `GRANT`s `SELECT` to `grafana_ro` for the staging -read-only role. +one. The fuel roll-up ships as `migrations/17_fleetops_fuel_view.sql` (the live migration head was +**16**, not 13 as older docs imply; 17 + 18 are now applied). `dashboard_ro` reads `v_fuel_daily` +via the schema-wide `SELECT` grant in `scripts/dashboard_ro_role.sql`. > **Reuse the existing reporting layer.** The analytics building blocks are `reporting.*` > (migrations 11/14) and the surviving `tracksolid.v_*` views (migration 07). The `ops.*` and @@ -163,9 +163,16 @@ Production is touched **only** by a merge to `main`. That branch discipline is w Staging hits the **production database**, so isolation is enforced at the **DB-role level**, not by a separate DB: -- The staging `dashboard_api` connects as a **read-only role** — reuse `grafana_ro`, or add a - dedicated `dashboard_ro` with `GRANT SELECT` on `reporting.*` and the `tracksolid.v_*` - views. Accidental writes from staging are then impossible. +- The staging `dashboard_api` connects as a **dedicated read-only role `dashboard_ro`** + (`scripts/dashboard_ro_role.sql` + `scripts/bootstrap_dashboard_ro.sh`). It grants exactly + what the API reads — `SELECT` on `reporting.*` + `tracksolid.*`, an explicit `SELECT` on the + `reporting.v_trips` **materialized view** (matviews aren't covered by `GRANT ... ON ALL + TABLES`), `EXECUTE` on the `reporting.fn_*` map functions, and `ALTER DEFAULT PRIVILEGES` so + future objects are auto-readable ("dynamic"). No write/REFRESH privilege, so accidental writes + are impossible. The password is generated on the host into `~/.dashboard_ro.pw` (0600), never + in the repo. **Two-stage:** stage 1 backs the *staging* bridge (done); stage 2 migrates the + *live prod* `fleetapi.rahamafresh.com` connection off the app role onto `dashboard_ro` (which + already grants the full read surface, incl. the live `fn_*` path). - The **`reporting.v_trips` materialized-view refresher is disabled on staging** — production owns it. The refresher needs write perms and is already pg-advisory-lock guarded (key `920_145`, FIX-D02); a read-only staging role would only log errors, so disable it explicitly @@ -182,7 +189,7 @@ client's production domains **last**. | Phase | Scope | Exit criterion | |---|---|---| -| **0 — Foundation** | This document; migrate all Coolify apps to Forgejo webhook deploys; provision the read-only DB role | Every existing Coolify app redeploys via webhook; read-only role can `SELECT` `reporting.*` + `tracksolid.v_*` and nothing else | +| **0 — Foundation** | This document; provision the read-only `dashboard_ro` role (**done**); migrate all Coolify apps to Forgejo webhook deploys (**pending**) | `dashboard_ro` can `SELECT` `reporting.*` + `tracksolid.*` + `v_trips` and nothing else; every Coolify app redeploys via webhook | | **1 — Staging backbone** | Staging `dashboard_api` bridge (`deploy_dashboard_api_staging.sh`, 8891, `fleetapi.fivetitude.com`, read-only, refresher off, staging CORS) | `curl https://fleetapi.fivetitude.com/health` ok; verifiably read-only; no staging rows in `reporting.refresh_log` | | **2 — FleetNow staging** | FleetNow repo: `staging` branch + parameterized API base + `fleetnow.fivetitude.com` Coolify app | Renders against staging API; `staging` push deploys staging only, `main` merge deploys prod only; prod FleetNow untouched | | **3 — FleetOps backend** | `/analytics/*` endpoints in `dashboard_api_rev.py` + `migrations/17_fleetops_fuel_view.sql`; refresher made disable-able (`VTRIPS_REFRESH_INTERVAL_S<=0`); tested on the staging API | Every route returns correct shape on `fleetapi.fivetitude.com`; fuel route returns "needs data" flags |