Files
mc_cars_gmbh_infraestructure/AGENT.md
T

160 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENT.md Operational notes for working on MC Cars
A compact field guide for anyone (human or AI) making changes to this stack. Skim this first.
---
## 1. The deployment model
1. **Portainer-native.** No `build:` steps. Every service pulls a pre-built image. The `web` service uses `nginx:1.27-alpine` with bind-mounted static files.
2. **Absolute host paths.** All bind mounts point to `/mnt/user/appdata/mc-cars/...`. Runtime state lives under `/mnt/user/appdata/mc-cars/data/`.
3. **No named volumes.** Wiping data means `rm -rf /mnt/user/appdata/mc-cars/data/db` — not `docker volume rm`.
4. **Secrets live in `.env`**; the `.env` ships with dev defaults. Any non-local deployment must rotate every key (see §6).
5. **External ports are in the `555xx` range** to avoid collisions on a busy host (`55521` Kong, `55530` Studio, `55532` Postgres, `55543` Kong TLS, `55580` web).
---
## 2. Migration lifecycle
Migrations come in two flavours:
### First-boot only (runs iff `/mnt/user/appdata/mc-cars/data/db` is empty)
- `supabase/migrations/00-run-init.sh` — wrapper that runs `01-init.sql` via `psql` so we can pre-create Supabase service roles (`anon`, `authenticated`, `service_role`, `authenticator`, `supabase_auth_admin`, `supabase_storage_admin`) that the official images otherwise assume already exist on their `supabase/postgres` image. We use plain `postgres:15-alpine`, so we create them ourselves.
- `supabase/migrations/01-init.sql` — vehicles table, RLS, bucket row, seed demo vehicles.
Postgres' official entrypoint only processes `/docker-entrypoint-initdb.d/` on an **empty** data dir. To re-run: wipe `/mnt/user/appdata/mc-cars/data/db`.
### Post-boot (every time, idempotent)
- `supabase/migrations/post-boot.sql` — seeds the admin `auth.users` row with `must_change_password=true`, creates the `vehicle-photos` bucket row if missing. Parameterized: `psql -v admin_email=... -v admin_password=...`.
- `supabase/migrations/02-leads.sql` — leads + customers + RPCs + `supabase_realtime` publication. Uses `create if not exists`, `create or replace`, and `do $$ ... $$` with exists-checks so every object is idempotent.
- `supabase/migrations/08-backend-pricing-and-security.sql``calculate_price` RPC (public, read-only pricing), refactored `create_lead` RPC (accepts no price params, computes server-side), unique partial indexes on `lead_attachments` (max 1 id_document + 1 income_proof per lead), hardened storage RLS (anon INSERT-only on customer-documents, admin SELECT+INSERT only).
- `supabase/migrations/09-site-settings.sql``site_settings` key-value table + RLS (public SELECT, admin full CRUD) + seed row `hero_image_url = '/images/ferrari-main-car.png'`.
Both are run by the `post-init` service, which gates on `auth.users` and `storage.buckets` being queryable before touching anything.
**Adding a new migration:**
- If it's schema the app can't start without on a fresh DB → extend `01-init.sql`.
- If it's ongoing / idempotent → new file `NN-*.sql`, mount it in `post-init`, append one more `psql -f` line to the entrypoint in `docker-compose.yml`.
- Current numbering: `01`, `02`, `08`, `09`. Use the next available number for new migrations.
---
## 3. Roles, RLS, and the mental model
| Role | How obtained | Can do |
| ----------------- | ---------------------------------- | ------------------------------------ |
| `anon` | Anon JWT (shipped to browser) | `select` on active `vehicles`; `insert` on `leads` only; `select` on `site_settings`; `insert` on `customer-documents` bucket; execute `calculate_price` and `create_lead` RPCs |
| `authenticated` | JWT from `signInWithPassword` | Full CRUD on vehicles/leads/customers/sales_orders/site_settings, `execute` RPCs, storage uploads |
| `service_role` | Service JWT (never in browser) | Bypasses RLS |
- Anon **cannot** read leads. The booking form is fire-and-forget by design. An abuser submitting with a stolen anon key sees only HTTP 201 with no row data.
- Anon **cannot** read/delete from `customer-documents` bucket — only INSERT (upload). Admin can SELECT + INSERT but not DELETE (prevents evidence destruction).
- RPCs are `SECURITY INVOKER`. They run as the caller, so `auth.uid()` returns the actual admin; RLS still applies.
- `qualify_lead` is idempotent — calling it twice returns the existing customer row instead of raising.
- `create_lead` computes prices server-side via `calculate_price` — no price params accepted from client (prevents price-tampering).
---
## 4. Realtime wiring
- Postgres command line forces `wal_level=logical`, `max_wal_senders=10`, `max_replication_slots=10`. Without `logical`, `supabase/realtime` crash-loops.
- `supabase/realtime:v2.30.23` connects as **`DB_USER=postgres`** (not `supabase_admin`). We're on the plain `postgres:15-alpine` image; the `supabase_admin` role does not exist.
- `SECRET_KEY_BASE` must be ≥ 64 chars; the hardcoded value in `docker-compose.yml` is a dev default — rotate it on real deploys.
- Replica identity `full` is set on leads/customers/vehicles so updates broadcast the complete row, not just the PK.
- Kong route `/realtime/v1/``http://realtime:4000/socket` (websocket upgrade headers handled by Kong's default config).
---
## 5. Kong traps
- On Windows/WSL2, Docker Desktop's `wslrelay` intercepts `127.0.0.1:8000`. On the production host Kong is exposed on **`55521`** (container `8000`).
- On a busy Unraid/Docker host all MC Cars ports live in the `555xx` range to avoid collisions.
- Kong needs `KONG_PLUGINS: bundled,request-transformer,cors,key-auth,acl,basic-auth`. Omitting `acl` breaks Studio.
- `kong.yml` has one `consumer` per role with `acl` groups; plugins `key-auth` + `acl` gate which `apikey` (anon vs service_role) can reach which route.
---
## 6. Password rotation & admin bootstrap
The `.env` `ADMIN_PASSWORD` is a one-shot **bootstrap** seed. On first login:
1. `admin.js` reads `user.user_metadata.must_change_password`.
2. If true, the UI shows the "Passwort setzen" screen and refuses to proceed.
3. Submit calls `supabase.auth.updateUser({ password, data: { must_change_password: false } })`.
4. The frontend also rejects `newPassword === loginPassword` before sending.
Result: the live admin password differs from `.env` from the very first session. The `.env` value is useful only for a brand-new `./data/db`.
Password min length is enforced server-side by `GOTRUE_PASSWORD_MIN_LENGTH=10`.
---
## 7. Frontend conventions
- **Only** the anon key leaves the server. `config.js` is generated at container start via an inline `entrypoint:` command in `docker-compose.yml` (writes `window.MCCARS_CONFIG={SUPABASE_URL,SUPABASE_ANON_KEY}`). The service_role key is never mounted into the `web` container.
- **No entrypoint script file.** An earlier design used `frontend/99-config.sh` bind-mounted into `/docker-entrypoint.d/`. Abandoned because Unraid's filesystem does not preserve Unix execute bits on bind mounts — the script was silently skipped. The inline `entrypoint:` in compose needs no file permissions.
- `frontend/config.js` is **git-ignored** (generated at runtime, would commit stale `localhost` values otherwise).
- `index.html` / `admin.html` load `config.js` with a `?v=<timestamp>` query string (generated by a small inline `document.write` snippet) to defeat both browser and proxy caching.
- The `web` service uses `nginx:1.27-alpine` directly (no `build:`). Static files and `nginx.conf` are bind-mounted. Updating the frontend is `git pull` + `docker compose up -d --force-recreate web`.
- `frontend/app.js` (public) creates a Supabase client with `persistSession: false` — the public site never needs a session.
- `frontend/admin.js` uses `persistSession: true, storageKey: "mccars.auth"` and a single realtime channel `mccars-admin` that subscribes to leads/customers/vehicles.
- Admin tabs: Aktive Leads, Inaktive Leads, Kunden, Fahrzeuge, **Einstellungen**. The Einstellungen (Settings) tab manages site-wide configuration (currently: hero image upload).
- `app.js` calls `calculate_price` RPC for sidebar pricing display — **no client-side price computation**.
- `app.js` calls `create_lead` RPC for form submission — prices are computed server-side and stored on the lead.
- `app.js` at boot loads `hero_image_url` from `site_settings` and applies it via CSS variable `--hero-bg` on `.hero`. The `styles.css` fallback is `url('images/ferrari-main-car.png')`.
- `app.js` writes `vehicle_id` (uuid) **and** denormalized `vehicle_label` ("BMW M3") into the lead at submit time, so the admin UI renders even if the vehicle is later deleted.
---
## 8. Common failure modes
| Symptom | Cause / Fix |
| ------------------------------------------------------- | ---------------------------------------------------------------- |
| `auth` container restart loop, "role supabase_auth_admin does not exist" | `00-run-init.sh` didn't run. Wipe `/mnt/user/appdata/mc-cars/data/db`, retry. |
| `realtime` container exits, "wal_level must be logical" | `db` `command:` missing `wal_level=logical`. |
| 502 on `/realtime/v1/` through Kong | Missing `acl` plugin in `KONG_PLUGINS`, or realtime not listed in `kong.yml` consumers. |
| Admin login works but Leads tab is empty | RLS: session is `anon` instead of `authenticated`. Check that `signInWithPassword` succeeded and token is attached to subsequent requests. |
| Booking form submits but no row appears | anon lacks `INSERT` grant on `public.leads`. Check `02-leads.sql` ran (see `post-init` logs). |
| `.sh` files reject with `\r: command not found` | CRLF line endings. `dos2unix` or re-save as LF. |
| Port 8000 "connection refused" on Windows | Docker Desktop wslrelay. Use `55521` (production port). |
| `config.js` not generated, website uses fallback URL | Unraid doesn't preserve execute bits — entrypoint script skipped. Fixed: `config.js` is now written by an inline `entrypoint:` command in docker-compose, no file needed. |
| Website loads but API calls fail ("Failed to fetch") | `config.js` cached by NPM or browser with old `localhost` URL. The `?v=timestamp` cache-buster on the `<script>` tag prevents this. Hard-refresh or check NPM "Cache Assets" toggle. |
| Git pull fails with "Your local changes would be overwritten" | `.env` was modified on the NAS. Use `git checkout -- .env && git pull`, then re-apply the two URL lines with `sed`. |
| `calculate_price` returns PGRST202 "Could not find the function" | Migration `08-backend-pricing-and-security.sql` not applied. Check `post-init` logs or run it manually. |
| Hero image not changing after admin upload | Browser/CDN caching the old image. The URL includes a unique path (`site/hero.ext`) with upsert — hard-refresh the public page. |
| `create_lead` fails with "function does not exist" | Same as calculate_price — migration 08 not applied. |
---
## 9. How to verify a working stack
```bash
docker compose ps # all services up (post-init exited 0)
curl http://localhost:55521/rest/v1/vehicles?select=brand -H "apikey: $ANON" # 6 demo cars
curl -X POST http://localhost:55521/rest/v1/rpc/calculate_price \
-H "apikey: $ANON" -H "Content-Type: application/json" \
-d '{"p_vehicle_id":"<uuid>","p_date_from":"2025-01-06","p_date_to":"2025-01-10"}' # pricing JSON
curl http://localhost:55521/rest/v1/site_settings?select=value&key=eq.hero_image_url \
-H "apikey: $ANON" # returns hero image URL
```
In the browser:
1. Open http://\<host\>:55580, verify the hero image loads (dynamic from settings).
2. Submit the booking form — sidebar shows server-computed pricing.
3. Open http://\<host\>:55580/admin.html, log in with `.env` bootstrap creds.
4. Rotate the password when prompted.
5. The lead you submitted appears in "Aktive Leads" — in real time, with pricing columns.
6. Click **Qualifizieren**. Row disappears from active, a new row appears in **Kunden** with the `lead_id` displayed.
7. Go to **Einstellungen** tab. Upload a new hero image. Verify it appears on the public site after refresh.
8. Open Supabase Studio at http://\<host\>:55530 and confirm `site_settings`, `leads` pricing columns, `customers.lead_id` FK.
---
## 10. Things explicitly NOT in this stack
- No Google Analytics / Google Reviews widget.
- No "Anmelden/Registrieren" on the public site. Admin access is intentionally unlinked.
- No logflare / analytics.
- No edge-functions container — RPCs live in Postgres.
- No client-side price calculation — all pricing is server-side via `calculate_price` RPC.