diff --git a/AGENTS.md b/AGENTS.md index 1fd26b8..05c7e5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -144,7 +144,8 @@ Encode TECH-SPEC exactly: ## 9. Deploy sketch (see DEPLOYMENT.md) -- Target: `.201`, Docker Compose (`frontend` + `fastapi`) + Caddy → `slab.tyubumini.local` (LAN/Tailscale). -- Inventory host ports/Caddy before binding (bolt.diy collision risk). -- Health: `GET /api/health`. -- Inference is a remote configurable endpoint, not on `.201` by default. +- Target: inventoried Docker host. Docker Compose (`frontend` + `fastapi`) + Caddy → `https://${APP_HOSTNAME}` (LAN/Tailscale). +- Fill `deploy/INVENTORY.md` before binding ports or merging Caddy (co-resident apps can collide; bolt.diy is one example). +- Health: `GET https://${APP_HOSTNAME}/api/health`. +- Inference is a remote configurable endpoint, not on the app host by default. +- Historical lab IP/hostname examples in older notes are non-normative. diff --git a/README.md b/README.md index d14751a..3ead516 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Mobile-first, self-hosted app for publishing one-of-a-one wood slabs to WooCommerce. Phone capture (online) → hybrid measure/crop on device → review → WooCommerce create. -Owner: Ty (Whidbey). Store: www.whidbeywoodstore.com. Deploy: .201 (Caddy, Docker). +Owner: Ty (Whidbey). Store: www.whidbeywoodstore.com. Deploy: Docker Compose + Caddy on an inventoried host (`APP_HOSTNAME`). ## Status **POC requirements freeze in progress (Gate A, 2026-08-31).** Read `AGENTS.md` before diff --git a/deploy/INVENTORY.md b/deploy/INVENTORY.md new file mode 100644 index 0000000..29f7077 --- /dev/null +++ b/deploy/INVENTORY.md @@ -0,0 +1,67 @@ +# Deploy inventory worksheet + +Fill this at **Gate B** on the designated host before any port bind or Caddy merge. +Do not assume a prior lab IP or hostname. See `docs/DEPLOYMENT.md`. + +## Host + +| Field | Value | +|---|---| +| Inventory date | | +| Operator | | +| Machine name / role | | +| `APP_HOST` (LAN or Tailscale IP/name used in DNS) | | +| `APP_HOSTNAME` (HTTPS site name, e.g. `slab.example.local`) | | +| OS / Docker version | | +| Notes | | + +## Ports and collisions + +| Check | Result | +|---|---| +| `docker ps` summary (names + published ports) | | +| Free HTTP port for frontend bind (`FRONTEND_PORT`, default 18080) | | +| Free port for fastapi bind (`FASTAPI_PORT`, default 18081) | | +| Caddy public HTTPS port in use by other sites | | +| Co-resident apps that own ports (example: bolt.diy on 8443-class) | | + +## Caddy + +| Field | Value | +|---|---| +| Caddy config path (host file or container) | | +| Existing site blocks that share this host | | +| TLS mode (`internal` CA vs other) | | +| Phone CA trust verified? (yes/no + how) | | + +## DNS / reachability + +| Field | Value | +|---|---| +| DNS source (Pi-hole / AdGuard / MagicDNS / hosts) | | +| A record: `APP_HOSTNAME` → `APP_HOST` | | +| Reachable from phone on LAN? | | +| Reachable from phone on Tailscale? | | + +## Volumes and backup + +| Field | Value | +|---|---| +| Docker volume dir convention | | +| Backup target path (local) | | +| Off-box mirror (if any) | | + +## Smoke after first up + +| Check | Pass? | +|---|---| +| `curl -k https://${APP_HOSTNAME}/api/health` | | +| Compose `frontend` + `fastapi` healthy | | +| Caddy `/api/*` reaches fastapi (not frontend) | | + +## Sign-off + +| Role | Name | Date | +|---|---|---| +| Inventory complete | | | +| Ty Gate B (optional) | | | diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 62ea0a3..6449ac4 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,29 +1,34 @@ -# Deployment Playbook (.201, Docker Compose + Caddy) +# Deployment Playbook (Docker Compose + Caddy) -Status: SPEC (deliverable #4) · 2026-08-24 · SlabUploader -Target: TyUBUMini at 192.168.1.201. Domain: `slab.tyubumini.local` (LAN/Tailscale +Status: SPEC · env-agnostic · 2026-09-01 · SlabUploader -> Gate A freeze (2026-08-31): hybrid client owns measure/crop SoT. Do not scaffold a server ruler/pipeline as authoritative. Woo UAT = draft + SLAB-UAT-* on PROD. -only — Decision). Inference is NOT on .201; it's a remote configurable endpoint -(.202 or elsewhere). +**Target:** any inventoried Docker host. Fill `deploy/INVENTORY.md` before bind. +Variables: `APP_HOST`, `APP_HOSTNAME`, `FRONTEND_PORT`, `FASTAPI_PORT`. +Health URL: `https://${APP_HOSTNAME}/api/health`. -> Note: exact Caddyfile/ports to be verified against the live .201 Caddy setup at -> implementation time (bolt.diy already runs Caddy TLS on tyubumini.local:8443). -> This playbook is the plan; the first implementation task is a .201 inventory pass. +> Gate A freeze (2026-08-31): hybrid client owns measure/crop SoT. Do not scaffold a +> server ruler/pipeline as authoritative. Woo UAT = draft + `SLAB-UAT-*` on PROD. +> Inference is **not** on the app host by default. It is a remote configurable endpoint. + +**Historical lab example (non-normative):** TyUBUMini at `192.168.1.201` with hostname +`slab.tyubumini.local` was one early candidate. Do not treat that IP or name as SoT. +Use the inventory worksheet. ## 1. Host inventory (do first — do not assume) -Before deploying, record the current state (HARD RULE: inventory before execute): -1. `docker ps` — existing containers, published ports (avoid collisions; bolt.diy - owns 8443/8444-class ports). -2. Caddy config location (host file or container) and existing `tyubumini.local` - site blocks. +Before deploying, fill `deploy/INVENTORY.md` (hard rule: inventory before execute): + +1. `docker ps` — existing containers, published ports (avoid collisions with + co-resident apps; bolt.diy is one known example of 8443-class port use). +2. Caddy config location (host file or container) and existing site blocks. 3. Free port choice for the compose stack (proposal: `18080` http, `18443` https — - verify free). + verify free on **this** host). 4. Docker volume dir convention on the host. -5. Tailscale: confirm `slab.tyubumini.local` is reachable from the phone's network - (same LAN or Tailscale). If the domain isn't in DNS yet, add it (Pi-hole/AdGuard - or Tailscale MagicDNS) — one record, A → 192.168.1.201 (LAN) + Tailscale name. +5. Reachability: confirm `${APP_HOSTNAME}` is reachable from the phone network + (same LAN or Tailscale). Add DNS (Pi-hole/AdGuard or Tailscale MagicDNS) if needed: + one A record → `${APP_HOST}` (LAN and/or Tailscale name). + +Write the chosen values into `deploy/INVENTORY.md` and the host `.env` before compose up. ## 2. Repository layout (at scaffold) @@ -35,10 +40,11 @@ SlabUploader/ ├── deploy/ │ ├── docker-compose.yml │ ├── Caddyfile.fragment # slab site block (merged into host Caddy) -│ ├── .env.example # SLAB_AES_KEY, port vars (no real secrets) +│ ├── INVENTORY.md # filled at Gate B on the designated host +│ ├── .env.example # SLAB_AES_KEY, APP_*, port vars (no real secrets) │ └── backup.sh # sqlite .backup + images tar, with retention -├── frontend/ # SvelteKit app (Phase 1) -└── backend/ # FastAPI app (Phase 0) +├── frontend/ # SvelteKit app +└── backend/ # FastAPI app ├── app/ │ ├── main.py │ ├── (no happy-path pipeline SoT here) # hybrid: TECH-SPEC modules live in frontend/ or shared pure TS; backend = store/Woo/inference/U2Net stub @@ -81,6 +87,7 @@ volumes: ``` Conventions (house rules): + - `mem_limit` on services (not host-level tuning). - Services bind to 127.0.0.1 only; Caddy is the only public surface. - No `latest` tags on base images — pin versions. @@ -88,21 +95,27 @@ Conventions (house rules): ## 4. Caddy (fragment) +API must win over the frontend catch-all. Put `handle /api/*` **before** the frontend +`handle` block: + ```caddyfile -slab.tyubumini.local { - tls internal # Caddy internal CA (matches bolt.diy pattern) - reverse_proxy frontend:80 { - header_up X-Real-IP {remote_host} - } +{$APP_HOSTNAME} { + tls internal # Caddy internal CA (or host-appropriate TLS) handle /api/* { reverse_proxy fastapi:8000 } + handle { + reverse_proxy frontend:80 { + header_up X-Real-IP {remote_host} + } + } } ``` -- HTTPS enforced (http→https redirect automatic). -- If the host Caddy already serves `*.tyubumini.local`, add this as a site block; - verify the internal CA cert is trusted on the phone (same flow as bolt.diy). -- LAN/Tailscale only — no public exposure. + +- HTTPS enforced (http→https redirect automatic when using Caddy TLS). +- If the host Caddy already serves a wildcard for this domain family, add this as a + site block. Verify the cert is trusted on the phone. +- LAN/Tailscale only — no public exposure for POC. ## 5. Secrets @@ -117,9 +130,10 @@ Key rotation (AES): `SLAB_AES_KEY` change requires re-encrypting settings — provide `python -m app.security.rekey NEW_KEY` (reads old key from env `SLAB_AES_KEY_OLD`, decrypts, re-encrypts, updates). Documented in playbook §7. -## 6. Backup & restore +## 6. Backup and restore + +`deploy/backup.sh` (run via cron on the app host, e.g. daily 03:00): -`deploy/backup.sh` (run via cron on .201, e.g. daily 03:00): ``` 1. sqlite3 /data/slab.db ".backup /backups/slab-YYYYMMDD.db" # WAL-safe 2. tar czf /backups/slab-images-YYYYMMDD.tgz -C /images . @@ -127,8 +141,9 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env 4. retain: 7 daily + 4 weekly; prune older 5. log result to /var/log/slab-backup.log; alert (Hermes) on failure ``` -- Backup target: local dir first; mirror to TrueNAS SMB share - (`\\truenas.local\…\backups\slab`) for off-box safety (same pattern as other stacks). + +- Backup target: local dir first; mirror to an off-box share for safety (same pattern + as other stacks on the host). - **Restore**: stop compose → restore DB file + untar images → start compose. Tested at least once during Phase 6 (UAT) before declaring backup done. @@ -139,7 +154,7 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env | Deploy (first) | inventory §1 → `docker compose -f deploy/docker-compose.yml build` → `up -d` → Caddy reload → smoke §8 | | Update | `git pull` → `docker compose build fastapi frontend` → `up -d` (DB migrations run on start via alembic) | | Logs | `docker compose logs -f fastapi` / `frontend` | -| Health | `curl -k https://slab.tyubumini.local/api/health` | +| Health | `curl -k https://${APP_HOSTNAME}/api/health` | | Rekey AES | §5 | | Rotate Woo creds | Woo UI → new consumer key/secret → Settings UI → `POST /api/settings/test-woo` | | Rotate inference key | Settings UI (if external) | @@ -148,7 +163,8 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env | Reset one slab | delete its photos dir + `UPDATE slabs SET status='draft'` (admin script) | ## 8. Smoke test (after every deploy) -1. `GET /api/health` → ok (from phone over HTTPS once CA trust is proven) + +1. `GET https://${APP_HOSTNAME}/api/health` → ok (from phone over HTTPS once CA trust is proven) 2. Settings: Woo creds + `test-woo` ok; `woo_create_status=draft`; inference OFF for early gates 3. Create a slab → upload test photos (fixture set) → poll to `ready` 4. Verify bdft/price match the fixture's expected values (golden test; bdft = sqft × thickness) @@ -156,7 +172,9 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env 6. Leave or delete the Woo draft UAT product (never customer-visible). Final live publish is a non-UAT SKU after Ty review. ## 9. Scaling path (not v1) + - Postgres when SQLite write contention appears (schema already portable). - Redis+arq when in-process queue shows lag. -- GPU on .201 only if RemBG latency becomes a bottleneck — otherwise keep it CPU - (normalization budget is 3–5s per PRD; U²Net on modern CPU meets that). +- GPU on the app host only if on-demand U2Net latency becomes a bottleneck. Keep CPU + first. Never lower `output_px_min` below 1600; undersized source means retake, not a + smaller min. RemBG is not part of the hybrid happy path. diff --git a/docs/IMPL-PLAN.md b/docs/IMPL-PLAN.md index 8caba5a..5ae92a6 100644 --- a/docs/IMPL-PLAN.md +++ b/docs/IMPL-PLAN.md @@ -64,7 +64,7 @@ Acceptance: - Gate B: client (or shared pure) tests green for TECH-SPEC vectors; bdft identity holds (no `/12`); undersized source warns and refuses fake upscale; rounding matches TV-10. -- `GET /api/health` up in compose over HTTPS hostname after host inventory + Caddy merge. +- `GET https://${APP_HOSTNAME}/api/health` up in compose after `deploy/INVENTORY.md` is filled and Caddy is merged. - Settings AES round-trip; secret fields never appear in GET. - Draft can store client-uploaded originals + PNGs + numbers without server recompute. - Coverage target: deterministic modules ≥ 95% where practical; property checks @@ -249,8 +249,8 @@ Goal: production deploy on the **inventoried** host + online user acceptance tes Deliverables: -- Host inventory pass (DEPLOYMENT §1 / `deploy/INVENTORY.md`) → final compose + Caddyfile. -- Deploy to `https://${APP_HOSTNAME}`; backup/restore runbook executed once (restore tested). +- Host inventory pass: complete `deploy/INVENTORY.md` per DEPLOYMENT §1 → final compose + Caddyfile. +- Deploy to `https://${APP_HOSTNAME}` from inventory; backup/restore runbook executed once (restore tested). - **UAT suite** for the hybrid online path: scripted real slabs (≥3 species, ≥1 cathedral, ≥1 manual-width path, ≥1 duplicate-SKU attempt) with expected bdft/price/image-fill asserted. **No** offline capture, OCR, or ruler cases in POC UAT. @@ -292,5 +292,5 @@ authoritative for POC. Do not treat ruler confidence as a POC risk row. - All 6 phases exit-gated by Ty (Phase 2 remains deferred stub unless product reopens it). - Hybrid online acceptance met (measured in Phase 6 UAT). - `pytest` + integration + UAT green; inference-off path green. -- Deployed on inventoried host, HTTPS, backed up, restore tested, runbook + key-rotation verified. +- Deployed on the inventoried host (`APP_HOSTNAME` from `deploy/INVENTORY.md`), HTTPS, backed up, restore tested, runbook + key-rotation verified. - Repo on gitea-atd (`Ty_Tech/SlabUploader`) is source of truth; docs current.