# SWIA Commission Agreement Intake — Operations & Architecture Runbook > Internal portal for capturing, approving, and finalizing producer commission > agreements. FastAPI + SQLite + Jinja, deployed as a Docker Space on Hugging > Face at **caf.swia.ai** (`pramodmisra/pif`). This doc is the single source of > truth for how data persists, where each list comes from, and how to operate it. > Last major update: 2026-06-19 (Rounds 6 & 7 + email-rule route-ordering fix). --- ## 1. Deployment & the two git remotes The repo has **two remotes on `main`** — keep them in lockstep: | Remote | URL | Role | |--------|-----|------| | `github` | github.com/pramodmisra/pif-app | Canonical source of truth (also runs the daily refresh Action) | | `origin` | huggingface.co/spaces/pramodmisra/pif | The live Space; pushing here triggers a rebuild/deploy | **Always `git push github main && git push origin main`.** They have silently drifted before. If `origin` rejects (non-fast-forward), `git fetch origin`, inspect `git log HEAD..origin/main`, and `git merge origin/main -X ours` rather than force-pushing (force-push drops the Space's commit ancestry). A push to `origin` triggers a **factory rebuild** (fresh container). A config change (secrets/volumes) or `restart_space()` triggers a restart. A push to `origin` while RUNNING rebuilds with **no downtime** (old container keeps serving through `RUNNING_BUILDING` → `RUNNING_APP_STARTING` → `RUNNING`). **Uptime self-heal (GitHub Action `uptime-check.yml`):** a cron every 15 min runs `scripts/uptime_check.py`, which reads the Space runtime stage and issues a **non-destructive plain restart** if it has fallen into a recoverable error (`RUNTIME_ERROR`/`PAUSED`/`STOPPED`/`SLEEPING`). It does **not** restart on `BUILD_ERROR`/`CONFIG_ERROR`/`NO_APP_FILE` (a code problem a restart can't fix) — it fails the job so GitHub emails the owner instead of restart-looping. Reuses the same `HF_TOKEN` secret as the daily refresh. Manual run: Actions tab → *Uptime Self-Heal* → *Run workflow*. --- ## 2. Persistent storage (CRITICAL) The DB is SQLite at `DATABASE_URL=sqlite:////data/app.db`. `/data` is **only persistent because a Storage Bucket is mounted there**: - Bucket **`pramodmisra/pif-db`** (private) is mounted **read-write at `/data`**. - HF's old flat "$5/mo storage tier" API is retired (`request_space_storage` 404s). Current model = **buckets + volumes**: ```python api.create_bucket("pramodmisra/pif-db", private=True) api.set_space_volumes("pramodmisra/pif", volumes=[Volume(type="bucket", source="pramodmisra/pif-db", mount_path="/data", read_only=False)]) api.restart_space("pramodmisra/pif", factory_reboot=True) ``` - SQLite-on-bucket is supported by HF (same pattern as Label Studio Spaces). - `huggingface_hub.SpaceRuntime.volumes` may show `None` even when mounted — confirm via raw API `GET /api/spaces/{id}?full=true` → `runtime.volumes`, or `api.list_bucket_tree("pramodmisra/pif-db")` (you'll see `app.db`). **⚠️ NEVER detach this bucket / remove the volume — the entire user database (logins, password resets, CE edits) lives there.** Without it, `/data` is ephemeral and every rebuild wipes the DB back to the seed. --- ## 3. Data flow: what populates each list ``` Google Sheet (clients) ─┐ ├─► build_refresh_data.py ─► refresh_data.json ─► HF dataset pif-data producers_seed.json ────┘ (CI, daily 08:00 UTC) (gitignored) (private) │ start.py pulls at boot ▼ Producer + Client tables in /data/app.db ``` ### Producers — `producers_seed.json` (TRACKED) is the source of truth - **Why a seed file:** `refresh_data.json` is **gitignored** (it carries 18,656 client records). On a fresh CI checkout it's absent, so the daily job used to emit an **empty** producer list → every reboot collapsed producers to the 11 seeded ones. Fixed 2026-05-29 by adding the tracked `producers_seed.json`. - `build_refresh_data.py` → `load_existing_producers()` reads `producers_seed.json` first (always present in CI), falling back to `refresh_data.json`. - Each row: `{code, prefix, suffix, name, email?}`. Built from `producers_codes_emails.csv` (the canonical roster, 35 people / 71 codes). - **To change the producer roster:** edit `producers_seed.json` (or regenerate from the CSV), commit, push both remotes, then run the refresh (Section 6). - **`Producer.is_active` is ADMIN-OWNED — the refresh no longer resets it.** Until 2026-06-23, both `start.py`'s boot import and `/api/data-refresh` did *"deactivate ALL producers, then re-activate every code in the refresh"* — so the nightly Space restart re-activated the whole 71-code roster and wiped any deactivations made in the admin panel (the "57 reset back to 74 overnight" bug). Now the refresh **upserts only**: existing producers get name/email/code updates but keep their current `is_active`; brand-new roster codes are added active (deactivate once and it sticks). Standing approvers (Bella `SANBE1` + the 12 locked-pair/Fitch codes) are still force-activated. To trim the visible producer list, **deactivate in the admin panel** — it now survives every refresh. Regression guard: `_smoke_producer_persist.py`. ### Producer emails — real values from the roster - `start.py`'s import (and the admin `/api/data-refresh` upload) set `Producer.email` from the JSON when present. Real emails (e.g. Kathryn Faulkner = `kdye@`, Michael Banner = `mbenner@`, Banks Snellings = `banks.snellings@`) win over the auto-generated `first-initial+lastname@` rule. - Producers with a blank email in the CSV (Richard S. Brewer, UAC Risk Management) fall back to the auto-generated address. ### Clients — Google Sheet (PowerApps-synced) - Sheet `1RYmxN855…` tab `gid=1047572149`, columns `LookupCode, Name`. - `build_refresh_data.py` fetches the public CSV export (guard: ≥15,000 rows), dedups by code → ~18,656 clients. Producers are **not** in the Sheet. ### Client Executives (CE) — separate `client_executives` table - CEs are an **independent list** (NOT the Producer table). Seeded once from `ce_seed.json` (7 SWIA CEs) when the table is empty; managed via **Admin → Manage Client Executives** (add/edit/toggle/delete + bulk paste). - The form's CE picker, name resolution, and approval email come from this table first, with a fallback to the Producer lookup for legacy/edge cases. --- ## 4. Authentication & user lifecycle - **Self-signup** (`/signup`): any `@snellingswalters.com` email → active `user`, logged in immediately (no email verification, no admin approval). - **Password reset**: self-service `/forgot-password` → emailed token (1 h); superadmin can also trigger a reset from the User Management panel. - **Persistence**: users live in `/data/app.db` (persistent bucket). `start.py` skips seeding when any user exists, so registrations/edits survive rebuilds. - **Seeded users**: only the two permanent superadmins (System Admin + Pramod) are in `seed_db.py`, so a fresh DB never resurrects deleted people. - **Removal**: superadmin-only Delete — blocked while active and blocked if the user has submissions (deactivate-don't-delete preserves the audit trail). --- ## 5. The client lookup (form performance) The intake form must **NOT** pre-render all 18,656 clients as `