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>
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>
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>
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>
- 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>
- 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>
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>