diff --git a/deploy/clients/paris-barbershop/.env.example b/deploy/clients/paris-barbershop/.env.example new file mode 100644 index 0000000..2a55ca5 --- /dev/null +++ b/deploy/clients/paris-barbershop/.env.example @@ -0,0 +1,7 @@ +# One per client. Copy to .env and fill (new-client.sh does this for you). +CLIENT_SLUG=happynails # lowercase, no spaces — used for the Compose project + container name +CLIENT_DOMAIN=happynails.de # the domain / subdomain Caddy serves for this client + +# --- reference only (NOT injected; the site/ HTML is static) --- +# CLIENT_NAME=Happy Nails Nürnberg +# BOOKING_URL=https://calendar.app.google/xxxxxxxx # bake into site/index.html during onboarding diff --git a/deploy/clients/paris-barbershop/docker-compose.yml b/deploy/clients/paris-barbershop/docker-compose.yml new file mode 100644 index 0000000..e24c609 --- /dev/null +++ b/deploy/clients/paris-barbershop/docker-compose.yml @@ -0,0 +1,30 @@ +# Per-client stack — ONE isolated static-site container per business client. +# +# Each client is its own Compose project (name: smb-client-), so they +# start / stop / update / remove fully independently and never share state. +# Booking is handled by the client's own Google Calendar Appointment Schedule +# (no container), and leads/bookings flow into the shared CRM via the n8n +# webhooks baked into the client's page — so this stack only serves the site. +# +# Don't edit this file per client. Copy the folder with ../new-client.sh, which +# fills .env (CLIENT_SLUG, CLIENT_DOMAIN) and seeds site/. +name: smb-client-${CLIENT_SLUG} + +services: + site: + image: nginx:1.27-alpine + container_name: client-${CLIENT_SLUG} + restart: unless-stopped + volumes: + - ./site:/usr/share/nginx/html:ro + networks: [proxy] # shared with Caddy; reached as client-:80 + mem_limit: 64m # static nginx idles at a few MB; cap keeps the 16 GB box safe + logging: + driver: json-file + options: + max-size: "5m" + max-file: "3" + +networks: + proxy: + external: true diff --git a/deploy/clients/paris-barbershop/site/index.html b/deploy/clients/paris-barbershop/site/index.html new file mode 100644 index 0000000..88654ff --- /dev/null +++ b/deploy/clients/paris-barbershop/site/index.html @@ -0,0 +1,506 @@ + + + + + + Paris Barber Shop — Nürnberg | 2 Filialen | Termin online buchen + + + + + + + + + + +
+ +
+
+ Herrenfriseur · 2 Filialen in Nürnberg +

Sauberer Schnitt.
Zwei Adressen.

+

Herrenschnitt, Bartpflege und mehr — in unseren Filialen im Röthenbach Center und an der Hafenstraße. Termin online buchen, Filiale frei wählbar.

+ +
+ 4,2–4,4 · 500+ Bewertungen + ✂ Herrenfriseur + 📍 Nürnberg-West +
+
+
+ + +
+
+
+ Filialen +

Welche Filiale passt für Sie?

+

Beide Filialen sind eigenständig buchbar — wählen Sie unten aus.

+
+
+ + +
+
+
+
+ + +
+
+
+ Leistungen +

Was wir für Sie tun

+

In beiden Filialen verfügbar — Preise und Details erfahren Sie bei der Buchung oder im Studio.

+
+
+
✂️

Herrenschnitt

Waschen, Schnitt, Styling — individuell auf Ihren Typ abgestimmt.

30 Min
+
🧔

Bartpflege

Konturieren, trimmen und pflegen für den perfekten Bart.

20 Min
+
+
+
+ + +
+
+
+ Termin buchen +

Wählen Sie Filiale, Leistung & Zeit.

+

Buchen Sie direkt hier rund um die Uhr — sofortige Bestätigung, kein Anruf nötig. Die Filialauswahl finden Sie im Buchungsfenster unter „Mitarbeiter".

+
+
+ +
+
+
+ + +
+
+
+ Kontakt +

Frage stellen oder Rückruf anfordern

+

Schreiben Sie uns kurz — wir melden uns am selben Tag.

+
+
+ + +
+
+
+ +
+
+ +
✓ Danke! Ihre Anfrage ist eingegangen — wir melden uns.
+

Mit dem Absenden stimmen Sie der Verarbeitung Ihrer Angaben zur Kontaktaufnahme zu.

+
+
+
+
+ +
+
+
+
PARIS BARBER SHOP
+

Herrenfriseur · Nürnberg

+
+
+

Röthenbach Center

+

Dombühler Str. 9 · 0911 47885170

+
+
+

Hafenstraße

+

Hafenstraße 12 · 0911 25387841

+
+
+
© 2026 Paris Barber Shop · Impressum · Datenschutz
+
+ + + + + + + + + + + +
+ +
+ + + + + + diff --git a/playbooks/mailcow-admin.md b/playbooks/mailcow-admin.md new file mode 100644 index 0000000..75f5ad0 --- /dev/null +++ b/playbooks/mailcow-admin.md @@ -0,0 +1,69 @@ +# Playbook — Mailcow Administration + +Provisioning and maintaining transactional email for `smb-online` clients. Mailcow lives at +`https://mail.mivanchenko.de/` (VPS, **not** the homelab box — separate host, already the MX for +`mivanchenko.de`, already has a correct PTR record). DNS for `mivanchenko.de` and its subdomains +is hosted at 1&1/IONOS (`ns1107.ui-dns.com`). + +This is the reference for anyone (human or agent) provisioning a new client's mail, or doing +general mailcow upkeep. + +## What "client mail" means here + +Each client gets a per-client sending identity: `noreply@.mivanchenko.de`. The `smb-crm` +backend (`backoffice/app/booking_mail.py:_sender_for`) **auto-derives** the From address from the +client's `domain` field — it always sends as `noreply@`, never anything else, and this is +not configurable per-send. Plan mailbox naming around that fact rather than fighting it. + +## Agent brief + +> You manage transactional email for the `smb-online` clients via mailcow +> (`https://mail.mivanchenko.de/`, admin UI) and 1&1/IONOS DNS for `mivanchenko.de`. +> +> Your job, when asked to set up mail for a new client `.mivanchenko.de`: +> 1. In mailcow: add `.mivanchenko.de` as a domain, generate a 2048-bit DKIM key for it, +> create one real mailbox (commonly `mailing@.mivanchenko.de` — a real inbox someone +> monitors), and add `noreply@.mivanchenko.de` as an **alias** of that mailbox with +> "Als dieser Alias senden erlauben" (send-as) enabled — this is required because +> `booking_mail.py` always sends From `noreply@`, never from the real mailbox address. +> 2. Tell the operator the exact DNS records to add at 1&1/IONOS: MX (`` → +> `mail.mivanchenko.de`, priority 10), SPF TXT (`` → `v=spf1 mx -all`), DKIM TXT (host + +> value from mailcow's key page), and DMARC TXT (`_dmarc.` → +> `v=DMARC1; p=none; rua=mailto:mailing@.mivanchenko.de`). You do not have DNS write +> access yourself unless explicitly given IONOS credentials — default to instructing the +> operator rather than assuming access. +> 3. Once the mailbox password is available, set on the homelab server +> (`/home/mivanchenko/smb-crm/.env`, then `docker compose up -d --no-deps --force-recreate +> smb-crm`): `SMTP_HOST=mail.mivanchenko.de`, `SMTP_PORT=587`, +> `SMTP_USERNAME=mailing@.mivanchenko.de`, `SMTP_PASSWORD=`, +> `MAIL_FALLBACK_FROM=noreply@.mivanchenko.de`. Append-only — never overwrite existing +> `.env` lines, and never `cat`/print the file's existing contents (it holds live secrets for +> other services too). +> 4. Verify: trigger one real booking through `/book/` and confirm the confirmation email +> actually arrives (check spam folder too, first time for a new domain). +> +> Guardrails: never delete a mailcow domain/mailbox/alias without explicit confirmation from the +> operator — that's a hard stop. Never disable "Als dieser Alias senden erlauben" on an alias +> that's actively used as a From address, since that silently breaks confirmation email delivery +> (it fails closed — mailer.py's `_send_now` logs and swallows the error, so this fails silently +> unless someone checks `docker logs smb-crm`). Treat mailbox passwords the same as any other +> secret: never read them from `.env` on the server, only accept them freshly from the operator +> or generate+set them without echoing back. + +## New client mail checklist (condensed) + +1. Mailcow → Mail Setup → Domains → add `.mivanchenko.de`. +2. Mailcow → Mail Setup → Mailboxes → add `mailing@.mivanchenko.de` + strong password. +3. Mailcow → ARC/DKIM keys → generate for `.mivanchenko.de` → copy the TXT record. +4. Mailcow → domain → Add alias → `noreply@.mivanchenko.de` → target + `mailing@.mivanchenko.de` → keep "Als dieser Alias senden erlauben" checked. +5. 1&1 DNS: MX, SPF, DKIM, DMARC records per the agent brief above. +6. `smb-crm/.env`: `SMTP_*` + `MAIL_FALLBACK_FROM`, then recreate the container. +7. Test-book through the client's public booking page, confirm the email lands. + +## Known constraint + +Sender identity is fixed to `noreply@` by the current code — there is no +per-client override to send from a different local part (e.g. `mailing@` or `termine@`). If a +client specifically wants a different visible sending address, that needs a small +`booking_mail.py` change (a `sender_local_part` column or similar), not just mailbox setup.