docs: update AGENT.md + ARQUITECTURE.md — inline entrypoint, NPM full config, env vars, Unraid notes

This commit is contained in:
Lago
2026-04-17 23:08:47 +02:00
parent 1498ec78c3
commit 00d08713ea
2 changed files with 83 additions and 15 deletions
+9 -4
View File
@@ -86,9 +86,11 @@ Password min length is enforced server-side by `GOTRUE_PASSWORD_MIN_LENGTH=10`.
## 7. Frontend conventions ## 7. Frontend conventions
- **Only** the anon key leaves the server. `frontend/99-config.sh` (bind-mounted into the nginx entrypoint dir) writes `config.js` at container start from `$SUPABASE_URL` / `$SUPABASE_ANON_KEY`. The service_role key is not mounted into `web`. - **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.
- The `web` service uses `nginx:1.27-alpine` directly (no `build:`). Static files + nginx.conf + the entrypoint script are bind-mounted read-only. Updating the frontend is `git pull` + restart the 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.
- `.gitattributes` enforces `eol=lf` on `*.sh` files so the entrypoint doesn't break with CRLF on Windows checkouts. - `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/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. - `frontend/admin.js` uses `persistSession: true, storageKey: "mccars.auth"` and a single realtime channel `mccars-admin` that subscribes to leads/customers/vehicles.
- `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. - `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.
@@ -105,7 +107,10 @@ Password min length is enforced server-side by `GOTRUE_PASSWORD_MIN_LENGTH=10`.
| 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. | | 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). | | 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. | | `.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). | | 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`. |
--- ---
+74 -11
View File
@@ -212,8 +212,6 @@ Host port mapping: `55521:8000` (8000 blocked by Docker Desktop's wslrelay on Wi
``` ```
frontend/ frontend/
├── Dockerfile (legacy, not used in Portainer deploy)
├── 99-config.sh (entrypoint: writes config.js at boot)
├── nginx.conf (serves /, gzip, cache headers) ├── nginx.conf (serves /, gzip, cache headers)
├── index.html app.js (public site, anon key, persistSession:false) ├── index.html app.js (public site, anon key, persistSession:false)
├── admin.html admin.js (admin CRM, persistSession:true) ├── admin.html admin.js (admin CRM, persistSession:true)
@@ -223,8 +221,10 @@ frontend/
└── datenschutz.html └── datenschutz.html
``` ```
- `config.js` is generated at container start by `99-config.sh` (bind-mounted into `/docker-entrypoint.d/`) from `$SUPABASE_URL` and `$SUPABASE_ANON_KEY` only. The service_role key is never mounted into the web container. - `config.js` is generated at container start by an **inline `entrypoint:` command** in `docker-compose.yml`. It writes `window.MCCARS_CONFIG={SUPABASE_URL,SUPABASE_ANON_KEY}` from the container's environment variables. No separate script file is used — Unraid's filesystem does not preserve Unix execute bits on bind mounts, so a mounted `.sh` file would be silently ignored.
- The `web` service uses `nginx:1.27-alpine` directly with bind-mounted files (no `build:` step). This is Portainer-compatible: updating the frontend is `git pull` + container restart. - `config.js` is **git-ignored**. The repo never ships a file with hardcoded URLs.
- Both `index.html` and `admin.html` load `config.js` with `?v=<timestamp>` appended via an inline `document.write` snippet, ensuring proxies and browsers never cache a stale URL.
- The `web` service uses `nginx:1.27-alpine` directly with bind-mounted files (no `build:` step). This is Portainer-compatible: updating the frontend is `git pull` + `docker compose up -d --force-recreate web`.
- `admin.js` is ES modules, imports `@supabase/supabase-js` from `esm.sh` (CDN, pinned version). One Supabase client per page. - `admin.js` is ES modules, imports `@supabase/supabase-js` from `esm.sh` (CDN, pinned version). One Supabase client per page.
- State lives in a single `state` object. Admin re-renders the active tab on realtime events. - State lives in a single `state` object. Admin re-renders the active tab on realtime events.
- Force-password-change modal is the only state that can preempt the rest of the admin UI after login. - Force-password-change modal is the only state that can preempt the rest of the admin UI after login.
@@ -246,15 +246,78 @@ What's code/config (committed):
**Portainer deployment:** **Portainer deployment:**
1. Clone repo to `/mnt/user/appdata/mc-cars` 1. Clone repo to `/mnt/user/appdata/mc-cars`
2. `chmod +x` the `.sh` files 2. `mkdir -p data/{db,storage}`
3. `mkdir -p data/{db,storage}` 3. Edit `.env`: set `SITE_URL` and `SUPABASE_PUBLIC_URL` to your domain (see below)
4. Portainer → Stacks → Add stack → paste compose + env vars → Deploy 4. Portainer → Stacks → Add stack → paste compose + env vars → Deploy
**Nginx Proxy Manager (single public domain):** > No `chmod` needed. The entrypoint that writes `config.js` is an inline shell command in `docker-compose.yml`, not a bind-mounted script, so file permissions are irrelevant.
- Proxy `/``mccars-web:80` (or `<host>:55580`)
- Custom locations `/auth/v1/`, `/rest/v1/`, `/realtime/v1/`, `/storage/v1/``mccars-kong:8000` (or `<host>:55521`) **Environment: two variables to change per environment**
- Do **not** expose `/pg/` or Studio publicly
- Update `.env` URLs to `https://cars.yourdomain.com` | Variable | Local dev | Production (NAS) |
|---|---|---|
| `SITE_URL` | `http://localhost:55580` | `https://your.domain.com` |
| `SUPABASE_PUBLIC_URL` | `http://localhost:55521` | `https://your.domain.com` |
All other GoTrue vars (`API_EXTERNAL_URL`, `GOTRUE_SITE_URL`, `GOTRUE_URI_ALLOW_LIST`) are derived from these two in `docker-compose.yml`.
**Nginx Proxy Manager (NPM) — required settings for single-domain HTTPS:**
Create one proxy host for your domain (e.g. `demo.lago.dev`):
*Details tab:*
- Scheme: `http`
- Forward Hostname / IP: `<NAS IP>` (e.g. `192.168.178.3`)
- Forward Port: `55580`
- Cache Assets: **OFF** (if left ON, NPM caches `config.js` and serves stale URLs)
- Websockets Support: **ON** (required for Realtime)
- Block Common Exploits: ON
*SSL tab:*
- SSL Certificate: your domain cert
- Force SSL: **ON**
- HTTP/2 Support: **ON**
*Advanced tab (⚙️) — paste this exactly:*
```nginx
location /auth/v1/ {
proxy_pass http://192.168.178.3:55521;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /rest/v1/ {
proxy_pass http://192.168.178.3:55521;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /realtime/v1/ {
proxy_pass http://192.168.178.3:55521;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /storage/v1/ {
proxy_pass http://192.168.178.3:55521;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
This routes `https://your.domain.com/` → nginx (static site) and all API paths → Kong. Do **not** expose `/pg/` or Studio publicly.
For real deployments: For real deployments:
1. Generate a new `JWT_SECRET` and matching `ANON_KEY` / `SERVICE_ROLE_KEY` (see Supabase self-hosting docs). 1. Generate a new `JWT_SECRET` and matching `ANON_KEY` / `SERVICE_ROLE_KEY` (see Supabase self-hosting docs).