docs: update README, AGENT, ARQUITECTURE for 555xx ports, Portainer deploy, absolute paths
This commit is contained in:
@@ -4,12 +4,13 @@ A compact field guide for anyone (human or AI) making changes to this stack. Ski
|
||||
|
||||
---
|
||||
|
||||
## 1. The portability contract (don't break it)
|
||||
## 1. The deployment model
|
||||
|
||||
1. **No named volumes.** All runtime state bind-mounts to `./data/`. `docker compose down -v` must never be required to migrate: copy the folder, `docker compose up -d`, done.
|
||||
2. **No absolute paths** in `docker-compose.yml` or any config.
|
||||
3. **Secrets live in `.env`**; the `.env` ships with dev defaults and is safe to commit until real deploy. Any non-local deployment must rotate every key (see §6).
|
||||
4. Anything that resets the DB in dev is `Remove-Item -Recurse -Force .\data` — not `docker volume rm`.
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
@@ -17,11 +18,11 @@ A compact field guide for anyone (human or AI) making changes to this stack. Ski
|
||||
|
||||
Migrations come in two flavours:
|
||||
|
||||
### First-boot only (runs iff `./data/db` is empty)
|
||||
### 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 `./data/db`.
|
||||
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=...`.
|
||||
@@ -61,7 +62,8 @@ Both are run by the `post-init` service, which gates on `auth.users` and `storag
|
||||
|
||||
## 5. Kong traps
|
||||
|
||||
- On Windows/WSL2, Docker Desktop's `wslrelay` intercepts `127.0.0.1:8000`. We expose Kong on host **`54321`** (container `8000`). Don't move it back to 8000.
|
||||
- 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.
|
||||
|
||||
@@ -84,7 +86,9 @@ Password min length is enforced server-side by `GOTRUE_PASSWORD_MIN_LENGTH=10`.
|
||||
|
||||
## 7. Frontend conventions
|
||||
|
||||
- **Only** the anon key leaves the server. `frontend/Dockerfile` 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. `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`.
|
||||
- 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.
|
||||
- `.gitattributes` enforces `eol=lf` on `*.sh` files so the entrypoint doesn't break with CRLF on Windows checkouts.
|
||||
- `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.
|
||||
- `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.
|
||||
@@ -95,30 +99,30 @@ Password min length is enforced server-side by `GOTRUE_PASSWORD_MIN_LENGTH=10`.
|
||||
|
||||
| Symptom | Cause / Fix |
|
||||
| ------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `auth` container restart loop, "role supabase_auth_admin does not exist" | `00-run-init.sh` didn't run. Wipe `./data/db`, retry. |
|
||||
| `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 `54321`. |
|
||||
| Port 8000 "connection refused" on Windows | Docker Desktop wslrelay. Use `55521` (production port). |
|
||||
|
||||
---
|
||||
|
||||
## 9. How to verify a working stack
|
||||
|
||||
```powershell
|
||||
```bash
|
||||
docker compose ps # all services up (post-init exited 0)
|
||||
curl http://localhost:54321/rest/v1/vehicles?select=brand -H "apikey: $env:ANON" # 6 demo cars
|
||||
curl http://localhost:55521/rest/v1/vehicles?select=brand -H "apikey: $ANON" # 6 demo cars
|
||||
```
|
||||
|
||||
In the browser:
|
||||
1. Open http://localhost:8080, submit the booking form.
|
||||
2. Open http://localhost:8080/admin.html, log in with `.env` bootstrap creds.
|
||||
1. Open http://\<host\>:55580, submit the booking form.
|
||||
2. Open http://\<host\>:55580/admin.html, log in with `.env` bootstrap creds.
|
||||
3. Rotate the password when prompted.
|
||||
4. The lead you submitted appears in "Aktive Leads" — in real time.
|
||||
5. Click **Qualifizieren**. Row disappears from active, a new row appears in **Kunden** with the `lead_id` displayed.
|
||||
6. Open Supabase Studio at http://localhost:3000 and confirm the `customers.lead_id` FK matches.
|
||||
6. Open Supabase Studio at http://\<host\>:55530 and confirm the `customers.lead_id` FK matches.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+37
-22
@@ -7,9 +7,9 @@ A thorough walkthrough of how the MC Cars system is wired. Complements [AGENT.md
|
||||
## 1. High-level diagram
|
||||
|
||||
```
|
||||
Browser (localhost)
|
||||
Browser (<host>)
|
||||
│ │
|
||||
:8080 (nginx) :54321 (Kong) :3000 (Studio)
|
||||
:55580 (nginx) :55521 (Kong) :55530 (Studio)
|
||||
│ │ │
|
||||
┌──────┴──────┐ │ │
|
||||
│ web │ │ │
|
||||
@@ -32,7 +32,7 @@ A thorough walkthrough of how the MC Cars system is wired. Complements [AGENT.md
|
||||
│ logical) │ └───────────┘
|
||||
└──────────┘ ▲
|
||||
│ │
|
||||
./data/db ./data/storage
|
||||
/mnt/.../data/db /mnt/.../data/storage
|
||||
```
|
||||
|
||||
Dashed lifetime services (exit 0 once done): `post-init`.
|
||||
@@ -46,8 +46,8 @@ Network: single user-defined bridge `mccars`. No host networking, no ingress con
|
||||
### 2.1 Public visitor reads cars
|
||||
|
||||
```
|
||||
browser → :8080/index.html (nginx, static)
|
||||
browser → :54321/rest/v1/vehicles?select=*&is_active=eq.true
|
||||
browser → :55580/index.html (nginx, static)
|
||||
browser → :55521/rest/v1/vehicles?select=*&is_active=eq.true
|
||||
→ Kong → rest (PostgREST) → postgres
|
||||
→ RLS: "vehicles_select_active" (for anon, is_active=true only)
|
||||
← JSON
|
||||
@@ -59,7 +59,7 @@ No auth header except the anon `apikey`. Kong strips the key and forwards with t
|
||||
|
||||
```
|
||||
browser → supabase.from('leads').insert({...})
|
||||
→ :54321/rest/v1/leads (POST, apikey=anon)
|
||||
→ :55521/rest/v1/leads (POST, apikey=anon)
|
||||
→ PostgREST → postgres
|
||||
→ RLS "leads_anon_insert": insert allowed, select denied
|
||||
← 201 (no body returned to anon)
|
||||
@@ -71,7 +71,7 @@ The frontend never reads leads back. If the INSERT fails (validation, RLS), the
|
||||
|
||||
```
|
||||
browser(admin.html) → supabase.auth.signInWithPassword
|
||||
→ :54321/auth/v1/token?grant_type=password
|
||||
→ :55521/auth/v1/token?grant_type=password
|
||||
→ Kong → gotrue → postgres(auth.users)
|
||||
← access_token (JWT, aud=authenticated), refresh_token
|
||||
← user.raw_user_meta_data.must_change_password (true on bootstrap)
|
||||
@@ -79,7 +79,7 @@ browser(admin.html) → supabase.auth.signInWithPassword
|
||||
if must_change_password:
|
||||
user blocked in "Passwort setzen" screen
|
||||
supabase.auth.updateUser({password, data:{must_change_password:false}})
|
||||
→ :54321/auth/v1/user (PUT with Bearer access_token)
|
||||
→ :55521/auth/v1/user (PUT with Bearer access_token)
|
||||
→ gotrue updates auth.users
|
||||
```
|
||||
|
||||
@@ -106,8 +106,8 @@ Realtime publication fires:
|
||||
|
||||
```
|
||||
admin.js → supabase.storage.from('vehicle-photos').upload(path, file)
|
||||
→ :54321/storage/v1/object/vehicle-photos/<path>
|
||||
→ Kong → storage-api → ./data/storage
|
||||
→ :55521/storage/v1/object/vehicle-photos/<path>
|
||||
→ Kong → storage-api → /mnt/.../data/storage
|
||||
← public URL served via imgproxy for on-the-fly transforms
|
||||
admin.js → UPDATE vehicles SET photo_url = <public url>
|
||||
```
|
||||
@@ -204,7 +204,7 @@ Declarative config in `supabase/kong.yml`. One consumer per role:
|
||||
|
||||
Plugins pipeline: `cors` → `key-auth` → `acl` → proxy. Public routes (storage public objects) are whitelisted without `key-auth`.
|
||||
|
||||
Host port mapping: `54321:8000` (8000 blocked by Docker Desktop's wslrelay on Windows).
|
||||
Host port mapping: `55521:8000` (8000 blocked by Docker Desktop's wslrelay on Windows; `555xx` range avoids collisions on busy Docker hosts).
|
||||
|
||||
---
|
||||
|
||||
@@ -212,7 +212,8 @@ Host port mapping: `54321:8000` (8000 blocked by Docker Desktop's wslrelay on Wi
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── Dockerfile (nginx:alpine, writes config.js at boot)
|
||||
├── Dockerfile (legacy, not used in Portainer deploy)
|
||||
├── 99-config.sh (entrypoint: writes config.js at boot)
|
||||
├── nginx.conf (serves /, gzip, cache headers)
|
||||
├── index.html app.js (public site, anon key, persistSession:false)
|
||||
├── admin.html admin.js (admin CRM, persistSession:true)
|
||||
@@ -222,30 +223,44 @@ frontend/
|
||||
└── datenschutz.html
|
||||
```
|
||||
|
||||
- `config.js` is generated at container start (`envsubst`) 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 `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.
|
||||
- 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.
|
||||
- `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.
|
||||
- Force-password-change modal is the only state that can preempt the rest of the admin UI after login.
|
||||
|
||||
---
|
||||
|
||||
## 7. Portability & deployment
|
||||
## 7. Deployment
|
||||
|
||||
**The contract:** everything under `c:\Coding\MC Cars GmbH\` (or whatever you rename it to) is the full stack. Copy it anywhere Docker runs, bring up the stack.
|
||||
**Host root:** `/mnt/user/appdata/mc-cars`
|
||||
|
||||
What's state (under `./data/`):
|
||||
- `./data/db/` — Postgres cluster
|
||||
- `./data/storage/` — object bucket files
|
||||
All bind mounts in `docker-compose.yml` use absolute paths under that root. The compose has no `build:` steps — every image is pulled from a registry, making it Portainer-compatible.
|
||||
|
||||
What's state (under `/mnt/user/appdata/mc-cars/data/`):
|
||||
- `data/db/` — Postgres cluster
|
||||
- `data/storage/` — object bucket files
|
||||
|
||||
What's code/config (committed):
|
||||
- `docker-compose.yml`, `.env` (dev defaults), `supabase/`, `frontend/`, `AGENT.md`, `ARQUITECTURE.md`, `README.md`
|
||||
- `docker-compose.yml`, `.env`, `supabase/`, `frontend/`, `AGENT.md`, `ARQUITECTURE.md`, `README.md`, `.gitattributes`
|
||||
|
||||
**Portainer deployment:**
|
||||
1. Clone repo to `/mnt/user/appdata/mc-cars`
|
||||
2. `chmod +x` the `.sh` files
|
||||
3. `mkdir -p data/{db,storage}`
|
||||
4. Portainer → Stacks → Add stack → paste compose + env vars → Deploy
|
||||
|
||||
**Nginx Proxy Manager (single public domain):**
|
||||
- Proxy `/` → `mccars-web:80` (or `<host>:55580`)
|
||||
- Custom locations `/auth/v1/`, `/rest/v1/`, `/realtime/v1/`, `/storage/v1/` → `mccars-kong:8000` (or `<host>:55521`)
|
||||
- Do **not** expose `/pg/` or Studio publicly
|
||||
- Update `.env` URLs to `https://cars.yourdomain.com`
|
||||
|
||||
For real deployments:
|
||||
1. Generate a new `JWT_SECRET` and matching `ANON_KEY` / `SERVICE_ROLE_KEY` (see Supabase self-hosting docs).
|
||||
2. Set a strong `POSTGRES_PASSWORD`, `ADMIN_PASSWORD`.
|
||||
3. Put Kong behind a TLS reverse-proxy (Caddy/Traefik/nginx) and set `SITE_URL`, `API_EXTERNAL_URL`, `SUPABASE_PUBLIC_URL` to the public https URLs.
|
||||
4. Wire real SMTP for password-reset mail (`SMTP_*`).
|
||||
5. Back up `./data/` on a schedule.
|
||||
3. Wire real SMTP for password-reset mail (`SMTP_*`).
|
||||
4. Back up `/mnt/user/appdata/mc-cars/data/` on a schedule.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MC Cars – Dockerized Supabase CRM
|
||||
|
||||
Self-hosted Supabase stack + bilingual (DE/EN) public website + lead-management admin panel. Everything lives under this folder. Copying the folder to another machine and running `docker compose up -d` reproduces the stack bit-for-bit — all runtime state is under `./data/` bind mounts, no named volumes.
|
||||
Self-hosted Supabase stack + bilingual (DE/EN) public website + lead-management admin panel. Designed for Portainer deployment — no `build:` steps, all services use pre-built images with bind mounts. The host deployment root is `/mnt/user/appdata/mc-cars`.
|
||||
|
||||
## What's inside
|
||||
|
||||
@@ -14,19 +14,28 @@ Self-hosted Supabase stack + bilingual (DE/EN) public website + lead-management
|
||||
| `realtime` | `supabase/realtime:v2.30.23` | Live `postgres_changes` subscriptions |
|
||||
| `meta` | `supabase/postgres-meta:v0.84.2` | Schema introspection for Studio |
|
||||
| `post-init` | `postgres:15-alpine` | Idempotent bootstrap: seed admin + migrations |
|
||||
| `kong` | `kong:2.8.1` | Single API gateway at `:54321` |
|
||||
| `studio` | `supabase/studio` | Supabase dashboard (`:3000`) |
|
||||
| `web` | local `nginx:alpine` build | Public site + admin panel (`:8080`) |
|
||||
| `kong` | `kong:2.8.1` | Single API gateway at `:55521` |
|
||||
| `studio` | `supabase/studio` | Supabase dashboard (`:55530`) |
|
||||
| `web` | `nginx:1.27-alpine` | Public site + admin panel (`:55580`) |
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker Desktop / Docker Engine with Compose v2
|
||||
- Free ports: `3000`, `5432`, `8080`, `54321`, `54443`
|
||||
- Docker Engine with Compose v2 (or Portainer with Stacks)
|
||||
- Free ports: `55521`, `55530`, `55532`, `55543`, `55580`
|
||||
|
||||
## Run
|
||||
|
||||
```powershell
|
||||
cd 'c:\Coding\MC Cars GmbH'
|
||||
### Via Portainer (recommended)
|
||||
|
||||
1. Clone the repo onto the host: `git clone <repo> /mnt/user/appdata/mc-cars`
|
||||
2. `chmod +x /mnt/user/appdata/mc-cars/frontend/99-config.sh /mnt/user/appdata/mc-cars/supabase/migrations/00-run-init.sh`
|
||||
3. `mkdir -p /mnt/user/appdata/mc-cars/data/{db,storage}`
|
||||
4. Portainer → Stacks → Add stack → paste `docker-compose.yml` → paste `.env` into Environment variables → Deploy.
|
||||
|
||||
### Via CLI
|
||||
|
||||
```bash
|
||||
cd /mnt/user/appdata/mc-cars
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
@@ -34,21 +43,20 @@ First boot pulls ~1.5 GB of images and runs migrations (`01-init.sql`, `post-boo
|
||||
|
||||
### Stop / reset
|
||||
|
||||
```powershell
|
||||
docker compose down # stop, keep data
|
||||
docker compose down -v # stop + delete named volumes (there are none anymore)
|
||||
Remove-Item -Recurse -Force .\data # FULL wipe (needed to re-run first-boot migrations)
|
||||
```bash
|
||||
docker compose down # stop, keep data
|
||||
rm -rf /mnt/user/appdata/mc-cars/data/db # FULL DB wipe (re-runs first-boot migrations)
|
||||
```
|
||||
|
||||
## URLs
|
||||
|
||||
| Purpose | URL |
|
||||
| ------------------------------- | -------------------------------- |
|
||||
| Public website | http://localhost:8080 |
|
||||
| Admin panel | http://localhost:8080/admin.html |
|
||||
| Supabase Studio | http://localhost:3000 |
|
||||
| API gateway (Kong) | http://localhost:54321 |
|
||||
| Postgres | `localhost:5432` |
|
||||
| Purpose | URL |
|
||||
| ------------------------------- | --------------------------------- |
|
||||
| Public website | http://\<host\>:55580 |
|
||||
| Admin panel | http://\<host\>:55580/admin.html |
|
||||
| Supabase Studio | http://\<host\>:55530 |
|
||||
| API gateway (Kong) | http://\<host\>:55521 |
|
||||
| Postgres | `<host>:55532` |
|
||||
|
||||
> Admin access is deliberately **not** linked from the public site. Bookmark it.
|
||||
|
||||
@@ -69,9 +77,9 @@ The admin is seeded with `must_change_password = true` in `raw_user_meta_data`.
|
||||
- RPCs: `qualify_lead(uuid, text)`, `disqualify_lead(uuid, text)`, `reopen_lead(uuid)` — transactional, `SECURITY INVOKER`, `authenticated` only.
|
||||
- Realtime: `supabase_realtime` publication broadcasts inserts/updates on leads, customers, vehicles.
|
||||
|
||||
## Portability
|
||||
## Deployment & portability
|
||||
|
||||
Runtime state under `./data/`:
|
||||
Runtime state under `/mnt/user/appdata/mc-cars/data/`:
|
||||
|
||||
```
|
||||
data/
|
||||
@@ -79,7 +87,13 @@ data/
|
||||
└── storage/ # vehicle-photos bucket content
|
||||
```
|
||||
|
||||
Everything else (config, migrations, frontend) is in the repo. Zip the folder, scp it, `docker compose up -d` — you have the same stack.
|
||||
All bind mounts in `docker-compose.yml` use absolute paths under `/mnt/user/appdata/mc-cars`. Clone the repo there, deploy as a Portainer stack, done. No `build:` steps — every service pulls a pre-built image.
|
||||
|
||||
To put behind **Nginx Proxy Manager** with a single public domain (`cars.yourdomain.com`):
|
||||
- Proxy `/` → `mccars-web:80` (or `<host>:55580`)
|
||||
- Custom locations `/auth/v1/`, `/rest/v1/`, `/realtime/v1/`, `/storage/v1/` → `mccars-kong:8000` (or `<host>:55521`)
|
||||
- Do **not** expose `/pg/` or Studio publicly.
|
||||
- Update `.env` URLs to `https://cars.yourdomain.com`.
|
||||
|
||||
## Project layout
|
||||
|
||||
@@ -96,17 +110,19 @@ MC Cars/
|
||||
│ ├── post-boot.sql # admin user (must_change_password) + bucket row
|
||||
│ └── 02-leads.sql # leads, customers, RPCs, realtime publication
|
||||
├── frontend/
|
||||
│ ├── Dockerfile # nginx + runtime anon-key injection
|
||||
│ ├── Dockerfile # (legacy, not used in Portainer deploy)
|
||||
│ ├── 99-config.sh # entrypoint: injects config.js with anon key
|
||||
│ ├── nginx.conf
|
||||
│ ├── index.html # public DE/EN site, booking form -> leads
|
||||
│ ├── admin.html # auth-gated CRM
|
||||
│ ├── app.js
|
||||
│ ├── admin.js # realtime + qualify/disqualify + password change
|
||||
│ ├── config.js # anon-only runtime config
|
||||
│ ├── config.js # anon-only runtime config (generated at boot)
|
||||
│ ├── i18n.js
|
||||
│ ├── styles.css
|
||||
│ ├── impressum.html
|
||||
│ └── datenschutz.html
|
||||
├── .gitattributes # enforces LF on .sh files
|
||||
├── AGENT.md # findings, conventions, traps
|
||||
└── ARQUITECTURE.md # architecture deep-dive
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user