# TODO / Backlog ## Rethink the tier model — decouple tier from business type **Goal:** tiers should NOT be tied to a kind of business. Which "tier" (and whether a client's site is self-hosted or runs on Google's free services) is a decision made **between me and the client**, case by case — not something baked into a demo. Implications to work through: - [x] **Demos are stack-agnostic starters.** ~~Drop tier/stack labels and jargon (self-hosted, Cal.com, Google-Stack, "≈ 0 €") from the public-facing demos & chooser.~~ DONE 2026-06-25: chooser reworded; all three demo pages neutralised — demo bars now read "Beispielseite · So könnte Ihr Online-Auftritt aussehen", booking widgets relabelled to generic "Online-Terminbuchung"/"Sichere Buchung", Google-Kalender/Cal.com/self-hosted/Tier labels removed from rendered copy *and* HTML comments, demo booking dates now render on the current week via JS so nothing looks stale. - [x] After a client signs, **I decide hosting**: self-host the page on the homelab, or stand it up on the client's free Google account. Already how it works in practice — the CRM has `tier` + `stack_notes` columns recording that per-client decision, and every deploy this session (happynails' credential, the client stacks in `deploy/clients/`) went through that path rather than a fixed per-tier template. Nothing left to build here. - [ ] **Needs your call:** Revisit the `Clients.tier` field semantics — keep A/B as an internal effort/price band, or replace with something hosting-agnostic (e.g. `hosting = google | selfhosted`, `plan = …`). - [ ] **Blocked on the above:** Onboarding form + workflow currently ask for Tier A/B — realign once the field semantics are settled. ## Onboard dashboard → manage Leads & Clients (CRUD) — DONE 2026-06-25 Built as a DB-first back-office (architecture pivot: **Postgres is the source of truth**, Sheets is a one-way mirror — see `backoffice/` and the `smb-db-source-of-truth` memory). Live at **onboard.mivanchenko.de/crm** behind the existing basic-auth. - [x] **Read:** Leads & Clients tables, served by `smb-crm` straight from Postgres. - [x] **Add:** "+ Neu" modal per entity; auto-IDs (C-#### / L-epoch), renewal auto-compute. - [x] **Edit:** per-row ✎ modal, token-protected PATCH. - [x] **Delete:** per-row 🗑 (token-protected), audit-logged — replaces the scratchpad cleanup. - [x] Behind Caddy basic-auth; mutations gated by `CRM_API_TOKEN`; all changes audit-logged. - [x] One-way **DB→Sheets mirror** keeps the Sheet overview current after every change. - [x] **n8n ingest swap:** `lead-intake` & `onboarding` now POST into the DB API (not Sheets); Telegram notify unchanged. `renewal-reminder` still reads Sheets (fine — Sheets mirrors DB). Follow-ups (not yet done): - [ ] Extend CRUD UI beyond Leads/Clients (projects, bookings, invoices, activity) if useful. Dashboard (`backoffice/app/static/index.html`) still only has Leads/Clients tabs. - [x] Daily backup of the `smb-db` Postgres volume — DONE: `deploy/backup/smb-db-backup.sh` (`pg_dump` + gzip, keeps 14 days, cron on the homelab). - [ ] Make the DB→Sheets overwrite atomic (currently clear-then-write; brief mid-write window a reader could catch the cleared sheet — harmless for a human overview). ## Credentials in the CRM — copy-to-clipboard — DONE 2026-07-15 **Goal:** be able to grab a client's login (e.g. their Easy!Appointments provider password) from the CRM dashboard itself instead of digging through Vaultwarden or a live n8n execution log — prompted by not being able to find the happynails booking password anywhere. New `credentials` entity (`backoffice/db/init.sql`, `backoffice/app/db.py`), **deliberately never mirrored to Sheets** (`mirror: False`, enforced in `mirror_async`/`mirror_entity`/`/api/sync` and skipped by `import_from_sheets.py`) and gated by `X-CRM-Token` even to read. Dashboard has a new **Credentials** tab with a masked value, "👁 zeigen" reveal toggle, "📋 Kopieren" copy button. `n8n/onboarding.json` now saves the auto-generated EA provider password instead of discarding it (root cause of the happynails password being unrecoverable — it was never persisted anywhere). Migrated + shipped to the live homelab (`smb-db` schema, `smb-crm` rebuilt, confirmed healthy). Happynails' (`C-0002`) reset EA password is recorded as `CR-1784116056728`. Follow-ups (not yet done — all need you, not more code): - [ ] Re-import the updated `n8n/onboarding.json` into the live n8n instance (the new "Save credential" node). Needs the real `__CRM_TOKEN__`/`__EA_AUTH__` credential bindings re-attached in the n8n UI — not safely scriptable from outside n8n. - [ ] The happynails credential's `username` is still blank — confirm it in the EA admin UI and fill it in via the dashboard's ✎ edit on the Credentials tab. - [ ] Your call, no rush: encrypt `credentials.secret` at rest (e.g. pgcrypto) instead of plaintext if the dashboard is ever exposed more broadly than Caddy basic-auth + host security. ## Deploy pipeline — trigger from the Gitea UI **Goal:** stop hand-running scp/ssh/docker-compose-build for every change (that's how the credentials feature above shipped). `git.mivanchenko.de` (Gitea, already the `origin` remote for every repo here, running alongside the other homelab containers) supports **Gitea Actions** — workflows in `.gitea/workflows/` that run on a self-hosted runner and can be started with a manual "Run workflow" button in the repo UI, not just on push. - [ ] Register a Gitea Actions runner (can run directly on the homelab host, or in its own container with the Docker socket mounted so it can `docker compose build`/`up -d`). - [ ] `.gitea/workflows/deploy.yml`: `workflow_dispatch` trigger (the UI button), optionally also on push to `main`. Steps: checkout → sync changed paths to `/home/mivanchenko/smb-crm/` (and whichever other `deploy/*` groups changed) → apply any pending SQL migrations → `docker compose build` + `up -d --no-deps ` for just the affected Compose group(s) → hit `/healthz` (or the equivalent) to confirm before declaring success. - [ ] Needs real secrets in Gitea's repo/runner secrets store (SSH deploy key or a runner already on the host, `CRM_API_TOKEN`, etc.) — **your call** on which, since it's a homelab access decision. - [ ] Once trustworthy, this replaces the manual migration step described in the credentials section above and the `new-client.sh` → scp → ssh flow in `deploy/clients/README.md`. ## Other - [ ] Harden n8n auth: compose still has deprecated `N8N_BASIC_AUTH_*` with password `password` (legacy/ignored by modern n8n owner-login). **Needs your call** — I can remove the dead env, but whether the owner account itself is strong enough is a judgment only you can make. - [ ] Orders: the pizzeria demo posts orders into the `Leads` tab via the lead-intake webhook as a stop-gap. Build a proper **Orders** flow + sheet tab (items, total, mode, status) when the order-management product is real. ## Done - [x] 2026-06-25 — Latency fix (n8n executions 1–4 min → ~6 s; DNS + PostHog telemetry). - [x] 2026-06-25 — Chooser reworded to client-friendly, jargon-free copy. - [x] 2026-06-25 — Pizzeria demo with online ordering added. - [x] 2026-06-25 — Barbershop "Google Kalender" dot-overlap styling fixed. - [x] 2026-06-25 — All three demo pages neutralised (stack-agnostic, non-stale dates). - [x] 2026-06-25 — DB-first back office: Postgres source of truth + Sheets mirror + CRUD + n8n ingest swap (lead-intake/onboarding → DB API). Live at onboard.mivanchenko.de/crm. - [x] 2026-07-14 — Shared self-hosted booking: Easy!Appointments stack (`deploy/booking/`), auto-provisioning wired into the onboarding workflow, `booking-sync` workflow syncing bookings to the CRM. - [x] 2026-07-14 — Per-client deploy tooling: `deploy/clients/` isolated-stack model + `new-client.sh` scaffolder; back-office updates. - [x] 2026-07-14 — Booking-sync workflow, done differently than originally scoped: the planned Tier-B-Cal.com-webhook / Tier-A-GCal-poll split was superseded by the tier-decoupling above — one shared Easy!Appointments instance handles booking for every client instead. - [x] 2026-07-15 — Credentials-in-the-CRM feature built and shipped live (see above).