diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..0693cd0 --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,11 @@ +# Placeholders only. Copy to deploy/.env (chmod 600). Never commit real values. +# Generate SLAB_AES_KEY: openssl rand -base64 32 + +SLAB_AES_KEY= +APP_HOSTNAME=slab.example.local +FRONTEND_PORT=18080 +FASTAPI_PORT=18081 + +# Plan of record: Woo store URL. HTTPS only. Not editable in Settings. +# Server must reject http:// at startup. +WOO_BASE_URL=https://www.whidbeywoodstore.com diff --git a/deploy/Caddyfile.fragment b/deploy/Caddyfile.fragment new file mode 100644 index 0000000..c5903e8 --- /dev/null +++ b/deploy/Caddyfile.fragment @@ -0,0 +1,14 @@ +# Merge into host Caddy. Replace {$APP_HOSTNAME} from inventory / .env. +# handle /api/* MUST come before the frontend catch-all. + +{$APP_HOSTNAME} { + tls internal + handle /api/* { + reverse_proxy fastapi:8000 + } + handle { + reverse_proxy frontend:80 { + header_up X-Real-IP {remote_host} + } + } +} diff --git a/deploy/INVENTORY.md b/deploy/INVENTORY.md index 29f7077..f6eef57 100644 --- a/deploy/INVENTORY.md +++ b/deploy/INVENTORY.md @@ -14,6 +14,7 @@ Do not assume a prior lab IP or hostname. See `docs/DEPLOYMENT.md`. | `APP_HOSTNAME` (HTTPS site name, e.g. `slab.example.local`) | | | OS / Docker version | | | Notes | | +| `WOO_BASE_URL` (`https://` store; not editable in app) | | ## Ports and collisions diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..0f0e3dc --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,41 @@ +# Implementable compose for SlabUploader (docs/refactor-v2). +# From repo root: +# cp deploy/.env.example deploy/.env && chmod 600 deploy/.env +# docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d +# Bind loopback only. Caddy is the TLS edge. Services on the compose network +# are named `frontend` and `fastapi` (must match Caddyfile.fragment). +# +# WOO_BASE_URL is deploy-time SoT, HTTPS only. FastAPI must refuse http:// +# at startup. Settings UI displays the URL read-only. + +services: + frontend: + build: + context: .. + dockerfile: frontend/Dockerfile + ports: ["127.0.0.1:${FRONTEND_PORT:-18080}:80"] + depends_on: [fastapi] + restart: unless-stopped + + fastapi: + build: + context: .. + dockerfile: backend/Dockerfile + env_file: + - .env + environment: + SLAB_AES_KEY: ${SLAB_AES_KEY} + WOO_BASE_URL: ${WOO_BASE_URL} + APP_HOSTNAME: ${APP_HOSTNAME:-} + # Woo Application Password + inference keys: AES-GCM in SQLite via Settings UI + volumes: + - slab_db:/data + - slab_images:/images + - ../prompts:/app/prompts:ro + ports: ["127.0.0.1:${FASTAPI_PORT:-18081}:8000"] + mem_limit: 2g + restart: unless-stopped + +volumes: + slab_db: + slab_images: diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index cf3ac6a..c93dedb 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -175,9 +175,10 @@ authoritative source for tag definitions; this table caches synced Woo tags. | `updated_at` | TEXT | | Keys (server settings; secrets encrypted at rest): -- `woo_base_url`, `woo_wp_username`, `woo_app_password` +- `woo_wp_username`, `woo_app_password` (username + WordPress application password for a dedicated low-priv WP user; not WooCommerce consumer keys) +- Store URL is **not** a settings row: `WOO_BASE_URL` env (compose), HTTPS only - `inference_base_url`, `inference_api_key`, `inference_model` - `inference_enabled`, `content_llm_enabled` - `brand_voice`, `geo_context` diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 86e3540..e746662 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -41,7 +41,7 @@ SlabUploader/ │ ├── docker-compose.yml │ ├── Caddyfile.fragment # slab site block (merged into host Caddy) │ ├── INVENTORY.md # filled at Gate B on the designated host -│ ├── .env.example # SLAB_AES_KEY, APP_*, port vars (no real secrets) +│ ├── .env.example # SLAB_AES_KEY, APP_*, WOO_BASE_URL (https only) │ └── backup.sh # sqlite .backup + images tar, with retention ├── frontend/ # SvelteKit app └── backend/ # FastAPI app @@ -58,33 +58,29 @@ SlabUploader/ └── Dockerfile ``` -## 3. docker-compose.yml (target shape) +## 3. docker-compose.yml (checked in) -```yaml -services: - frontend: - build: ./frontend - ports: ["127.0.0.1:${FRONTEND_PORT:-18080}:80"] # Caddy-only; not public - depends_on: [fastapi] - restart: unless-stopped +Canonical file: `deploy/docker-compose.yml`. Placeholders: `deploy/.env.example`. +From **repo root**: - fastapi: - build: ./backend - environment: - SLAB_AES_KEY: ${SLAB_AES_KEY} # from .env (32-byte base64) - # no Woo/inference keys here — they live encrypted in SQLite (Settings UI) - volumes: - - slab_db:/data - - slab_images:/images - - ./prompts:/app/prompts:ro # prompt files, read-only mount - ports: ["127.0.0.1:${FASTAPI_PORT:-18081}:8000"] # Caddy-only - mem_limit: 2g # env convention: mem_limit preferred - restart: unless-stopped - -volumes: - slab_db: - slab_images: ``` +cp deploy/.env.example deploy/.env && chmod 600 deploy/.env +# fill SLAB_AES_KEY, APP_HOSTNAME, WOO_BASE_URL (https:// only) +docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d +``` + +Caddy site block: `deploy/Caddyfile.fragment` (merge into host Caddy). Compose +service names `frontend` and `fastapi` must match that fragment. + +**`WOO_BASE_URL` (issue #11 — plan of record):** + +- Set in `deploy/.env` and injected into the FastAPI container at start. +- **HTTPS only.** FastAPI must refuse missing / `http://` values at startup. +- Not a Settings input and **not** a SQLite settings row. +- Settings UI shows the URL as **read-only** text (which store this deploy hits). +- Change store = edit `.env` + recreate the fastapi container. + +Woo Application Password stays out of compose (AES-GCM via Settings; issue #5). Conventions (house rules): @@ -95,8 +91,8 @@ Conventions (house rules): ## 4. Caddy (fragment) -API must win over the frontend catch-all. Put `handle /api/*` **before** the frontend -`handle` block: +Canonical file: `deploy/Caddyfile.fragment`. API must win over the frontend +catch-all. Put `handle /api/*` **before** the frontend `handle` block: ```caddyfile {$APP_HOSTNAME} { @@ -121,7 +117,8 @@ API must win over the frontend catch-all. Put `handle /api/*` **before** the fro | Secret | Where | Notes | |---|---|---| -| `SLAB_AES_KEY` | `.env` on host (mode 600) → compose env | 32-byte base64; NEVER in the repo | +| `SLAB_AES_KEY` | `deploy/.env` (mode 600) → compose env | 32-byte base64; NEVER in the repo | +| `WOO_BASE_URL` | `deploy/.env` → compose → FastAPI env | `https://` only; Settings **read-only** | | Woo WP username + application password | SQLite `settings` (AES-GCM) via Settings UI | dedicated low-priv WP user; not consumer keys | | Inference API key | SQLite `settings` (AES-GCM) via Settings UI | usually empty for local llama.cpp | | `.env.example` | repo | placeholders only, committed | diff --git a/docs/OPENAPI.md b/docs/OPENAPI.md index f244c24..9b1f896 100644 --- a/docs/OPENAPI.md +++ b/docs/OPENAPI.md @@ -120,7 +120,7 @@ the call returns. WooCommerce is the authoritative source for `feat-*` definitio ### SettingsView (read; secrets masked) ```jsonc -{ "woo_base_url": "https://www.whidbeywoodstore.com", +{ "woo_base_url": "https://www.whidbeywoodstore.com", // WOO_BASE_URL env; read-only "woo_wp_username": "slab-uploader", // non-secret identity; password never returned "woo_credentials_configured": true, "inference_base_url": "http://192.168.1.202:8080/v1", @@ -209,6 +209,8 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView). ### Settings - `GET /api/v1/settings` → `200` `SettingsView` - `PUT /api/v1/settings` — Update settings. Body: partial `{ key: value }`. + - `woo_base_url` is **not** writable (ignore or `422`). SoT is `WOO_BASE_URL` + env (HTTPS, compose). GET still returns it for read-only Settings text. - Woo credential keys: `woo_wp_username` (plain) and `woo_app_password` (write-only; AES-GCM at rest). Reject any payload that still sends `woo_consumer_key` / `woo_consumer_secret`. @@ -216,7 +218,7 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView). never echoed). - `POST /api/v1/settings/test-woo` → `200` `{ "ok": bool, "detail": "..." }` - Uses stored username + application password over HTTPS Basic Auth against - the store REST v3 base. Fails closed if credentials missing or store HTTP. + `WOO_BASE_URL` REST v3. Fails closed if credentials missing or URL is not https. - `POST /api/v1/settings/test-inference` → `200` `{ "ok": bool, "vision_capable": bool, "detail": "..." }` ---