Commit Graph

34 Commits

Author SHA1 Message Date
mivanchenko 3a20cb5d2b CI: run backoffice test suite on push/PR, document local test setup
Adds .gitea/workflows/test-backoffice.yml (paths-filtered to
backoffice/app/** and backoffice/db/**, self-hosted homelab runner,
plain git clone matching deploy-backoffice.yml's conventions) so a
regression no longer needs a human to notice it.

The psycopg[binary]==3.2.1 pin turned out to be correct, not stale: it
only ships wheels through cp312, so it resolves cleanly under the
project's python:3.12-slim target but not under newer interpreters.
Documented in requirements.txt and docs/agents/testing.md, along with
the fact that the suite spins up its own throwaway postgres:16-alpine
container (tests/conftest.py) with no manual DB setup required.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:34:43 +02:00
mivanchenko 528a13ca7c Owner calendar view + manual booking + owner cancel/reschedule (#20)
Adds an owner-authenticated weekly agenda (grouped by day, today highlighted)
with manual walk-in/phone booking creation, cancel, and reschedule -- all
routed through booking_api.py's create/cancel/reschedule logic (refactored
into shared helpers) so the EXCLUDE overlap constraint and confirmation
email stay on the single existing code path. Manual creation can skip the
opening-hours/min-notice/max-advance/buffer checks via an explicit override,
but never the overlap constraint itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:16:19 +02:00
mivanchenko 59c35ce39f Owner accounts: auth, password reset, CRM dashboard visibility (#19)
Flask-session login scoped to one client_id (never a request param),
self-service + operator-triggered password reset via single-use tokens,
and an Owner accounts tab on the CRM dashboard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:00:37 +02:00
mivanchenko 644c99ee30 Booking confirmation email + customer self-service cancel/reschedule (#18)
Sends a confirmation email (best-effort, fire-and-forget SMTP via mailer.py)
on booking creation, with a manage-booking link embedding the ticket-2 signed
token. Adds /manage/<token>, a stateless cancel/reschedule page that reuses
the existing slot-picker against booking_api's create/cancel/reschedule API,
distinguishing an invalid/expired link from an already-cancelled one. Sender
address uses the client's own domain when configured, falling back to a
mivanchenko.de address otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 15:40:45 +02:00
mivanchenko b895663c3a Public booking page + iframe embed (#17)
Adds the customer-facing /book/<slug> page: service/slot picker, booking
form, and confirmation screen, built on #16's existing booking JSON API.
Includes iframe auto-fit height reporting (mirroring
deploy/booking/booking_layout.js's eaBookingHeight message), brand-color
theming via a ?color= query param, a honeypot field with a fake-success
response indistinguishable from a real booking, and a clear "just taken"
message on slot-conflict. Caddy per-IP rate limiting is documented in
deploy/booking/RATE_LIMIT.md for manual application (no Caddyfile is
tracked in this repo).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 15:15:29 +02:00
mivanchenko 2f6e0c1459 Availability engine + booking API: create/cancel/reschedule (#16)
Adds the plumbing that makes "can a customer actually get booked" true
end to end at the API layer, on top of #15's schema/tenancy layer.

- resource_hours table + min_notice_minutes/max_advance_days/buffer_minutes
  on resources -- config #15 didn't include but #16 depends on.
- availability.py: pure slot-generation function, correct across a
  Europe/Berlin DST transition (tested both directions).
- booking_api.py: JSON blueprint for slot listing, booking creation
  (auto_confirm -> confirmed/pending), and signed-JWT cancel/reschedule,
  registered into app.py.
- booking_db.py gains resource-hours CRUD, a tenant-scoped busy-bookings
  query for buffer/slot validation, and a read-only client lookup.

A true concurrent-threads test (not just sequential requests) surfaced a
real gap: Postgres can raise DeadlockDetected instead of ExclusionViolation
when two overlapping inserts race the exclusion constraint directly, which
went uncaught and would have 500'd instead of giving the clean 4xx the
ticket requires -- now caught alongside ExclusionViolation.

Also fixed: reschedule used the request's raw UTC offset to pick the
business day instead of the client's own timezone (could pick the wrong
day's hours/bookings near local midnight); the cancel/reschedule JWT no
longer falls back to reusing CRM_API_TOKEN as its signing secret.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 14:52:33 +02:00
mivanchenko b5c0fc8a5a Booking schema + tenancy-safe data-access module (#15)
Adds the plumbing every other booking ticket builds on: resources,
services, users, and password_reset_tokens tables, plus the clients
columns (slug, timezone, auto_confirm, ics_token) and the bookings
resource_id/EXCLUDE-constraint double-booking protection described in #14.

booking_db.py is the only place raw SQL runs against these tables --
every function takes client_id and injects the tenant filter itself,
and create/update_booking additionally verify the resource_id belongs
to that client before writing, closing a guessed-ID cross-tenant hole.

Tests spin up a real throwaway Postgres 16 container (matching prod) and
exercise the EXCLUDE constraint, tenancy isolation, and password-reset
single-use semantics end to end, per #14's "real Postgres, no mocking"
testing decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 14:28:06 +02:00
mivanchenko b70fd06e7d Add agent-skills config for Gitea + triage
Scaffolds the per-repo config the mattpocock engineering skills expect:
- AGENTS.md with the ## Agent skills block (issue tracker, triage labels, domain docs)
- docs/agents/issue-tracker.md — Gitea REST API workflow (gh/glab don't apply)
- docs/agents/triage-labels.md — canonical role -> Gitea label mapping (1:1)
- docs/agents/domain.md — single-context CONTEXT.md/ADR consumer rules

Triage state labels (needs-triage, needs-info, ready-for-agent, ready-for-human,
wontfix) created in the tracker; all open issues seeded with needs-triage.
Run triage with /mattpocock-skills:triage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgbgipE41xwPcQJnhnq1J1
2026-07-23 10:31:59 +02:00
mivanchenko 098b6dfd39 TODO: resolve conflict, map backlog to Gitea issues
Reconcile the stashed rewrite with the newer 2026-07-15 state (credentials
store, deploy pipeline). Backlog is now tracked as Gitea issues #1–#12; TODO.md
is the high-level map. Notes: #8 (booking-sync) closed as already done via the
shared Easy!Appointments instance; #4 narrowed (smb-db pg_dump exists, n8n/Gitea/
offsite/restore remain). Added a "manual operator follow-ups" section for the
human-only items (n8n re-import, credential username, deploy-pipeline test, etc).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgbgipE41xwPcQJnhnq1J1
2026-07-16 10:42:15 +02:00
mivanchenko 0c2e4450f2 Fix deploy pipeline: strip refs/heads/ prefix from dispatch ref input
inputs.ref resolves fully-qualified (refs/heads/main) rather than the
declared default "main", so git clone --branch was failing with
"Remote branch refs/heads/main not found".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 18:28:00 +02:00
mivanchenko 6b7d47fa4c Fix deploy pipeline checkout: no Node on host runner, repo is private
actions/checkout@v4 failed with "Cannot find: node in PATH" (the
runner intentionally has no Node toolchain). Replaced with a plain
git clone authenticated via a new DEPLOY_TOKEN Gitea Actions secret,
since the repo needs auth even for a read-only clone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 18:18:44 +02:00
mivanchenko 8733175f64 Add Gitea Actions deploy pipeline for the backoffice
Manual (workflow_dispatch) pipeline that syncs backoffice/db and
backoffice/app to the homelab, re-applies the idempotent schema,
rebuilds/restarts smb-crm, and health-checks it — the same sequence
that was previously run by hand over SSH.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 15:27:55 +02:00
mivanchenko 94ae2d578d Add credentials store to the CRM, docs cleanup, deploy pipeline TODO
Adds a `credentials` entity to the back office (never mirrored to
Sheets, gated by the CRM token even to read) so client logins like
the auto-generated Easy!Appointments provider password can be viewed
and copied from the dashboard instead of getting lost — the actual
cause of the happynails password going missing. Onboarding now saves
that generated password instead of discarding it. Also adds
Documentation.md, brings README/TODO in line with the current
Postgres-first architecture, and tidies the backlog.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 14:13:34 +02:00
mivanchenko 8eba724428 update readme 2026-07-15 12:31:15 +02:00
mivanchenko 156166b4e5 Add booking stack, client deploys, and back-office updates
- deploy/booking: shared Easy!Appointments stack with brand-matched
  wizard (flatpickr recolor, single-tenant provider hide, iframe
  auto-fit height reporter)
- deploy/clients: per-client isolated nginx compose stacks with
  _template scaffold, new-client.sh, and happynails live site
- deploy/backup: smb-db backup script
- n8n: booking-sync workflow; onboarding tweaks
- playbooks: lead-to-customer lifecycle + outreach
- templates: nail-studio landing previews
- backoffice: app/db/init/compose updates

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:10:15 +02:00
mivanchenko 4257bd3e55 Dashboard: add link to the onboarding form
Header button on the CRM linking back to the operator onboarding page at "/"
(same host, same basic-auth).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:43:03 +02:00
mivanchenko b84a5d4c6f Dashboard: full-width tables, no horizontal scrollbar
Remove the 1280px content cap and the table's overflow scroll; let cells wrap
(word-break) instead of forcing a horizontal scrollbar, so rows span the full
screen width and show full values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:41:21 +02:00
mivanchenko 10fea59e05 Fix dashboard 404: derive API base from page path
Opening the dashboard at /crm (no trailing slash) made the relative `api/leads`
fetch resolve to /api/leads -> 404. Derive the API base from
location.pathname so it works at both /crm and /crm/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:39:34 +02:00
mivanchenko d5a304ebe9 n8n ingest swap: lead-intake & onboarding write to the DB
Both flows now POST into the smb-crm DB API instead of appending to Sheets, so
Postgres stays the source of truth and the DB->Sheets mirror keeps the Sheet
current. Telegram notifications unchanged.

- lead-intake: Webhook -> Build lead row -> POST /api/leads -> Telegram
  (dropped the direct Sheets append + the redundant activity-log nodes; the DB
  service logs + mirrors activity itself).
- onboarding: Webhook -> Compute -> POST /api/clients (DB assigns C-id) ->
  Build project w/ returned id -> POST /api/projects -> Telegram. DB computes
  renewal_date; go-live = start + 14d.
- Token injected at deploy time; repo keeps the __CRM_TOKEN__ placeholder.

Verified end-to-end on the live webhooks: lead and client+project land in the
DB and mirror to Sheets; test data cleaned; DB/Sheets consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:33:53 +02:00
mivanchenko e59996a385 Back office: one-way DB -> Sheets mirror
- After each mutation, the affected tab + activity_log are pushed to Sheets by
  a single serialized background worker (Postgres stays source of truth; a
  mirror failure never fails the DB write). Concurrency race fixed by
  serializing through one worker; PYTHONUNBUFFERED for visible logs.
- _cell() formats dates/timestamps/numbers/bools and neutralises formula
  injection. POST /api/sync does a full DB->Sheets resync of every tab.

Verified: add/edit/delete propagate to the Sheet; full resync aligns all tabs;
DB and Sheet consistent after cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:26:42 +02:00
mivanchenko 9f42e46d6c Back office: add / edit / delete for Leads & Clients
- Token-protected mutation endpoints (POST/PATCH/DELETE), audit-logged to
  activity_log. Token injected into the basic-auth-gated dashboard.
- Add auto-generates IDs (next C-#### / L-<epoch>), sets created/received/status
  defaults, and computes client renewal_date from start + billing cycle
  (parity with the onboarding workflow).
- Dashboard: per-row edit (✎) and delete (🗑), "+ Neu" modal form per entity.

Verified end-to-end: add lead/client, edit, delete, renewal compute, token
gating (403), 404s. DB-only for now; DB->Sheets mirror is the next step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:18:28 +02:00
mivanchenko 18e346d67b Back office: Postgres source-of-truth + read dashboard
Stand up the DB-first CRM backbone (architecture pivot: Postgres is the
source of truth, Google Sheets becomes a one-way downstream mirror).

- backoffice/ stack: smb-db (Postgres 16) + smb-crm (Flask/waitress service).
- Schema mirrors the six Sheet tabs (clients, leads, projects, activity_log,
  bookings, invoices) with typed columns + updated_at triggers.
- Service-account Sheets client (PyJWT) for the one-time import + future mirror.
- import_from_sheets.py: idempotent seed of Postgres from the live Sheets.
- Read dashboard (Leads & Clients tables) at onboard.mivanchenko.de/crm,
  behind the existing Caddy basic-auth; JSON API reads straight from Postgres.

Deployed + verified: import seeded DB, dashboard/API live, no-auth blocked,
onboarding form unaffected. Add/edit/delete + DB->Sheets sync + n8n ingest
swap are the next steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:12:39 +02:00
mivanchenko a04ad82e73 Neutralise demo pages: stack-agnostic copy, non-stale dates
- Demo bars now read "Beispielseite · So könnte Ihr Online-Auftritt aussehen"
  (was "Tier A (Google-Stack)" / "Tier B (Self-Hosted)" / "für Gäste").
- Booking widgets relabelled generic: "Online-Terminbuchung" / "Sichere Buchung"
  (dropped Google Kalender + branded dots, Cal.com, self-hosted).
- Removed Tier/Google/Cal.com refs from rendered copy AND HTML comments.
- Demo booking dates now render on the current week via JS (no stale dates).
- TODO.md: tick off completed items; add onboard back-office (Leads/Clients CRUD).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:47:22 +02:00
mivanchenko cde0cb0fa8 Add pizzeria demo w/ online ordering; fix gcal dots; reword chooser
- New demo-pizzeria (Bella Napoli): interactive menu -> cart (qty +/-,
  live total) -> order form (pickup/delivery, address, time, notes) that
  POSTs the order summary to the lead-intake webhook (client_id
  DEMO-PIZZA, source order-pizzeria). Verified end-to-end (~5s).
- Fix barbershop "Google Kalender" header: the 3 dots were faked via
  box-shadow with no layout width, so text overlapped them. Reserve space.
- Chooser: client-friendly wording (no "self-hosted"/Cal.com jargon, no
  "~0 EUR"), reframed as example businesses, 3-up responsive layout.
- TODO.md: decouple tier from business type; demos as stack-agnostic
  starters; hosting (Google vs self-host) decided per client. Plus notes
  on n8n auth hardening and a real Orders flow/tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:32:01 +02:00
mivanchenko e151bbd734 Add Rückruf (callback) lead popup to chooser + both demos
Floating button + centered modal on every page, opened from the demo
bar and a fixed FAB. Minimal required fields (name + phone), optional
business / what-they-want / message. POSTs to the lead-intake webhook
(client_id DEMO/DEMO-A/DEMO-B, source callback-*). Also refreshes the
stale "noch nicht aktiv" notices now that the forms are live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:11:09 +02:00
mivanchenko f1577bcefa Phase 5: add internal operator onboarding page
Self-contained admin form that POSTs to the /webhook/onboard endpoint
(creates Clients + Projects + Activity Log rows). Hosted behind HTTP
basic auth at onboard.mivanchenko.de (Caddy basic_auth -> smb-demos
container). Not linked from the public demos; creates billable records
so it stays operator-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:00:36 +02:00
mivanchenko a59a22b38b Phase 3: add onboarding workflow
Webhook /webhook/onboard → read Clients → compute next C-#### →
append Clients row (status onboarding, renewal_date auto from billing
cycle) → seed Projects row (checklist from services, +14d go-live) →
log Activity → Telegram confirmation. Sheets writes retry on fail.
Verified end-to-end (C-0002 created across all three tabs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:13:12 +02:00
mivanchenko a6eb8a415c Phase 4/5: host demos via Apache container behind Caddy
deploy/smb-demos: compose + runbook for the static demo host
(httpd on the proxy network, routed by Caddy at demos.mivanchenko.de).
Pending a DNS A record before Caddy can issue TLS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:03:25 +02:00
mivanchenko 2bd6499ae5 Phase 4: harden Sheets writes with retry-on-fail
Add retryOnFail (4 tries, 3s apart) to all Google Sheets nodes in
lead-intake and renewal-reminder so a transient API throttle/race
re-tries instead of silently dropping a row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:53:17 +02:00
mivanchenko 4b7e201b91 Phase 4: wire demo lead forms to live n8n webhook
- demo-tier-a / demo-tier-b: forms POST JSON to /webhook/lead-intake
  (hidden client_id + source, named fields), async submit with
  success/error toast.
- lead-intake webhook: enable CORS (allowedOrigins=*) so the static
  demos (different origin) can submit; re-export n8n/lead-intake.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:48:56 +02:00
mivanchenko 1e6c46e9f0 Phase 3: add renewal-reminder workflow
Daily 08:00 schedule → read Clients → flag active clients whose
renewal_date is due (14/7/3/1/0 days out or overdue) → Telegram
summary → log to Activity Log. Quiet on days with nothing due.
Verified end-to-end via CLI run against a seeded due client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:42:29 +02:00
mivanchenko 339fb1a1a7 Phase 3: add Telegram lead notification to lead-intake
- Notify Telegram node (bot @smb_leads_bot) between Leads append and
  Activity Log; notified flag set TRUE. Verified end-to-end (lead →
  Telegram + Leads row + Activity Log).
- Re-export n8n/lead-intake.json with the notify node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:36:46 +02:00
mivanchenko 0009a138a9 Phase 3: export working lead-intake workflow; gitignore .secrets
- n8n/lead-intake.json: Webhook → Build lead row (sanitized) →
  Leads append → Activity Log append. Verified end-to-end.
- .gitignore: ensure .secrets/ excluded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:15:07 +02:00
mivanchenko 5668bfad3f Phase 1: tier demo landing pages, CRM schema, playbooks
- templates/landing: two self-contained demos (Tier A barbershop / Google stack,
  Tier B physio / self-hosted) + chooser page
- sheets/SCHEMA.md: Google Sheets CRM workbook spec
- playbooks: per-client setup checklists for both tiers
- README + .gitignore
- Demos avoid unverifiable claims (no "DSGVO-konform", no "Ende-zu-Ende")

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:14:15 +02:00