Commit Graph
94 Commits
Author SHA1 Message Date
Stefan Lazic 0d821ce1f8 add client
CI / ci (push) Canceled after 0s
adds a button to add a new client from the barber menu
2026-09-14 12:25:45 +01:00
Stefan Lazic 91f3fd77d7 Update install-wizard.tsx
CI / ci (push) Canceled after 0s
2026-09-13 19:59:42 +01:00
Stefan Lazic 4573587b78 Email Notifictions
CI / ci (push) Canceled after 0s
Core logic: types.ts (defaults), notification.service.ts (skipEmail + dedup), booking-notifications.ts, loyalty-notifications.ts, reminder-scheduling.ts, booking.service.ts + scheduling.service.ts (staff-selection capture), features/owner/actions/appointments.ts.
Schema: prisma/schema.prisma + new migration 20260912165903_add_staff_selected_by_customer.
UI: both notification-preferences pages (customer, staff) now use the same default logic as the actual send decision.
Tests: notification.service.integration.test.ts, booking-notifications.integration.test.ts, reminder-scheduling.integration.test.ts, loyalty-notifications.integration.test.ts, plus a new createTestAppointment fixture.
Docs: docs/notifications.md rewritten to match.

Default changes

Exactly as specified — full table is in docs/notifications.md. In short: STAFF_NEW_APPOINTMENT, STAFF_APPOINTMENT_RESCHEDULED, STAFF_APPOINTMENT_REMINDER, OWNER_NEW_BOOKING, OWNER_NO_SHOW now default off; everything else that was already toggleable is untouched.

No longer email (fixed, not a toggle)

BOOKING_COMPLETED, LOYALTY_STAMP_ADDED, LOYALTY_REWARD_REDEEMED — always. BOOKING_BARBER_CHANGED — only when the customer originally booked "any available barber." The customer's 2-hour reminder is removed entirely (24h stays); staff keeps both, just off by default. All via a new skipEmail flag on notify(), checked in one place — no new preference rows for these.

Deduplication

One new EQUIVALENCE_GROUPS map in notification.service.ts (STAFF_NEW_APPOINTMENT>OWNER_NEW_BOOKING, STAFF_APPOINTMENT_CANCELLED>OWNER_BOOKING_CANCELLED). Inside notify(), before creating deliveries, it checks for an already-created equivalent notification for the same (businessId, appointmentId, recipientUserId); if found and at-least-as-relevant, the new one gets zero deliveries on every channel (row still created for audit trail). Only listed types participate, so OWNER_NO_SHOW and any future actionable owner type are never touched.
2026-09-12 18:27:11 +01:00
Stefan Lazic 7224c87cef Header Update
CI / ci (push) Canceled after 0s
Updated the header of these pages to match the rest of the pages.
2026-09-12 17:24:47 +01:00
Stefan Lazic 1932cf7774 Update page.tsx
CI / ci (push) Canceled after 0s
2026-09-12 15:49:28 +01:00
Stefan Lazic de32175916 Google Calendar
CI / ci (push) Canceled after 0s
Made google calendar auth not business specific.

global-settings.ts — new GoogleCalendarSettings section, encrypted at rest the same way as the SMTP password/VAPID private key.

google-calendar-rollout.ts — pushes the shared client ID/secret into every business's .env and recreates its container. GOOGLE_CALENDAR_REDIRECT_URI is left alone (it must keep tracking each business's own domain).

provisioning.ts / compose-template.ts — new businesses now pick up the global client automatically at creation, same as Email/Push already do.

Settings UI — new "Google Calendar" card at the bottom of Manager's Settings page (google-calendar-settings-form.tsx), with Save + "Reapply to all businesses."
2026-09-12 15:09:48 +01:00
Stefan Lazic 65e141d527 Update page.tsx
CI / ci (push) Canceled after 0s
2026-09-12 11:35:09 +01:00
Stefan Lazic 0e39ca7cdc Owner appointment details
CI / ci (push) Canceled after 0s
same changes applied to this route
2026-09-12 10:52:48 +01:00
Stefan Lazic b1fce9c002 Appointment details layout 2
CI / ci (push) Canceled after 0s
"Add to Calendar" removed from the staff appointment details page — it never showed for owner/customer views anyway, only here.
Buttons now stack in one column, in exactly the order you asked: Check In → Reschedule → Mark No-Show → Cancel. Reschedule moved from a separate top row into appointment-status-actions.tsx itself (new rescheduleUrl prop) so it can sit in the right spot relative to the status-specific buttons — this also means it correctly disappears for statuses that were never reschedulable (e.g. once In Progress), same as before.
Add Loyalty Stamp is now an icon-only button (add-stamp-button.tsx gained an iconOnly prop) sitting directly opposite "X/Y stamps" inside the card — same click-to-open-modal behavior, just compact.
2026-09-12 10:21:29 +01:00
Stefan Lazic f7fad3d1a0 Appointment details layout
CI / ci (push) Canceled after 0s
fixed button layout on appointment details page.

added an explainer for all the notifications
2026-09-12 10:01:55 +01:00
Stefan Lazic f24d22fdf1 Notification Settings
CI / ci (push) Canceled after 0s
src/services/notifications/types.ts — every STAFF_*/OWNER_* type is now preference-controlled (customer-facing types are untouched except the existing BOOKING_REMINDER, per your scope call).
src/services/notifications/notification.service.ts — IN_APP now respects the same preference check as EMAIL/PUSH. For every type outside the controlled set (the vast majority) this changes nothing — isChannelEnabled still short-circuits to true.
notification-preferences-form.tsx — generalized from one hardcoded toggle to a real per-type grid (groups prop), each row with Email/Push/In-app columns.
Staff settings → Notifications (page.tsx) — now shows 6 staff-facing toggle groups always, plus 5 owner-facing ones only for whoever actually holds VIEW_REPORTING (the same permission that defines "owner" elsewhere in the app) — so a plain barber never sees owner-only switches that could never apply to them.
Customer settings → Notifications — unchanged behavior, just updated to the new shared component's shape.
2026-09-12 09:41:37 +01:00
Stefan Lazic bfa13c5f15 Update README.md
CI / ci (push) Canceled after 0s
Fix notification-delivery cron docs: cron may not be installed, /var/log/ needs root
2026-09-11 18:16:17 +01:00
Stefan Lazic 1a76d1cacb Notifications
CI / ci (push) Canceled after 0s
Added a cron job processor so emails and notifications can get processed.

Added super admin settings with a gui to edit.
2026-09-11 17:46:28 +01:00
Stefan Lazic cbeccc6c1f Auth Fix
CI / ci (push) Canceled after 0s
Fixed an issue where the user would be redirected to a localhost address because an auth url was not set in the env.
2026-09-06 21:01:54 +01:00
Stefan Lazic e9e28a243f Installation
CI / ci (push) Canceled after 0s
install token now shown in dashboard until install completes.
2026-09-06 20:14:25 +01:00
Stefan Lazic d7a8b15482 Demo business removal
CI / ci (push) Canceled after 0s
Removed the demo business as its easier to just provision one manually.
2026-09-06 19:01:41 +01:00
Stefan Lazic 217c0b0101 Demo Business
CI / ci (push) Canceled after 0s
Updated references to a demo business.
2026-09-06 18:53:02 +01:00
Stefan Lazic 4763b50e62 Dockge Removal
CI / ci (push) Canceled after 0s
Removed dockge from the app as it is not required due to the manager page picking up most of its abilities.
2026-09-06 18:33:01 +01:00
Stefan Lazic 973cb9efc6 Update README.md
CI / ci (push) Canceled after 0s
2026-09-06 15:54:44 +01:00
Stefan Lazic 479cefd085 Update README.md
CI / ci (push) Canceled after 0s
2026-09-06 13:02:20 +01:00
Stefan Lazic 106f89a6da VAPID Settings
CI / ci (push) Canceled after 0s
1. Summary of changes

The request assumed a push-notification subsystem needed to be built — it already existed in full: the PushSubscription model, subscribe/unsubscribe service and Server Actions, the PushSubscribeToggle opt-in UI, WebPushProvider, and public/sw.js's push/notificationclick handlers were all already implemented and tested. The one real gap: NEXT_PUBLIC_VAPID_PUBLIC_KEY is a Next.js build-time-inlined variable, but every business runs the same prebuilt shared image — so on the actual deployment architecture, every business's browser bundle has always shipped with this key baked in empty, regardless of that business's .env. This was a genuine pre-existing bug, and centralizing VAPID globally is the fix for it, not just a tidiness improvement.

Added a Push Notifications section to the Manager's Settings page, mirroring the SMTP architecture exactly: same JSON storage, same MANAGER_SETTINGS_ENCRYPTION_KEY encryption, same rollout mechanism.
Keys are generate-only (web-push.generateVAPIDKeys()) — no hand-pasted key fields, avoiding a mismatched pair that would silently break every push send.
Regeneration requires explicit confirmation with a clear warning about the impact, shown live and verified in-browser.
Replaced the broken NEXT_PUBLIC_VAPID_PUBLIC_KEY mechanism with a new runtime API route, GET /api/push/vapid-public-key — this is what actually makes centralized VAPID config reach the browser, verified live end-to-end.
Fixed a related bug in web-push-provider.ts: a 401/403 (VAPID signature mismatch — exactly what happens to every subscription after a key rotation) wasn't being treated as an invalid subscription, so rotated-out subscriptions would have failed forever without ever being cleaned up.
Two additional real bugs found and fixed during live verification (not just written correctly on paper): the settings forms weren't refreshing their server-fetched data after a successful save, so the Public/Private Key fields and the "Reapply" button stayed stale-looking immediately after a successful Generate.
No Prisma/schema changes — the existing model already satisfied every field in the request.
2. Architecture used for sharing VAPID configuration

Identical to SMTP: Manager is the one place a human ever generates the key pair; it's stored in stacks/manager/global-settings.json, then pushed into every business's .env (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT, PUSH_PROVIDER="web-push") via the same upsertEnvValues + composeUp(dir, {forceRecreate:true}) mechanism, verified live to actually rewrite a scratch business's .env and recreate its container. New businesses get it baked in at provisioning time automatically.

3. How and where the VAPID private key is stored

AES-256-GCM encrypted (same key and mechanism as the SMTP password) inside stacks/manager/global-settings.json. Verified directly: the file contains an {iv, authTag, ciphertext} envelope, never plaintext. Never returned to the Manager UI after generation — only a boolean hasPrivateKey.

4. Prisma/database changes

None. The existing PushSubscription model already covers every field requested.

5. Service worker/PWA changes

None to the service worker itself — public/sw.js's push/notificationclick handlers already worked correctly. The only client change is push-subscribe-toggle.tsx, which now fetches the public key from the new API route instead of a dead build-time constant, and gained a new "unconfigured" state shown up front (before a wasted permission prompt) when push genuinely isn't configured.

6. How tenant push subscriptions are stored

Unchanged, and already correct: each business has its own separate database (confirmed throughout this session's work), so tenant isolation is enforced at the infrastructure level, not just a businessId column — there's no query path from one business's container into another's subscriptions at all.

7. How multiple devices are handled

Unchanged — already correct and already tested (endpoint unique, upsert-on-resubscribe, per-user userId/customerId fan-out in reminder-worker.ts).

8. What happens when a subscription becomes invalid

Unchanged for 404/410. Newly fixed: 401/403 (VAPID mismatch, e.g. after a key rotation) is now also treated as subscriptionInvalid — the stale subscription gets revoked automatically on the next delivery attempt rather than failing silently forever.

9. What happens if VAPID keys are regenerated

Documented in the Manager UI's own confirmation warning (verified live): every existing browser subscription across every business stops receiving pushes, and each device needs to click "Enable Push Notifications" again — but nothing needs to be manually cleaned up, since the app now detects and revokes the broken subscriptions automatically (see #8).

10. Files changed

New: manager/src/lib/push-rollout.ts, manager/src/app/dashboard/settings/push-settings-form.tsx, src/app/api/push/vapid-public-key/route.ts + its test.

Modified: manager/src/lib/global-settings.ts (push section + a fix ensuring email/push sections never clobber each other), manager/src/lib/provisioning.ts, manager/src/lib/compose-template.ts, manager/src/app/dashboard/settings/{actions.ts,page.tsx,email-settings-form.tsx} (the last one gained the same router.refresh() fix), push-subscribe-toggle.tsx, web-push-provider.ts + its test, .env.example, .env.business.example, manager/package.json/package-lock.json (added web-push).

11. New environment variables

None new. MANAGER_SETTINGS_ENCRYPTION_KEY already exists from the SMTP work and is reused. NEXT_PUBLIC_VAPID_PUBLIC_KEY is removed (dead — superseded by the API route).

12. VPS deployment/update steps

Rebuild/redeploy both images as usual (no build-arg needed, unlike a naive fix would have required). Open Manager → Settings → Push Notifications, set a contact email, click "Generate VAPID Keys" — this automatically rolls out to every existing business. Nothing else needs to run by hand.

13. Manual testing steps
EMAIL_PROVIDER=console PUSH_PROVIDER=console npm run dev

Generate keys in Manager, confirm the overwrite-warning on a second attempt, confirm a business's .env gains the 4 keys. Hit curl localhost:PORT/api/push/vapid-public-key — confirmed live to return exactly the configured public key. Sign in to a business and visit Settings → Notifications — confirmed live the toggle correctly reaches the real permission-check step (proving the key delivery works) rather than showing "not configured."

The global VAPID / Web Push feature is already fully designed, implemented, and verified — all three research agents finished, I synthesized their findings, wrote the plan, got it approved, built it, found and fixed three real bugs along the way (the broken build-time public-key delivery, the missing 401/403 invalidation handling, and the stale-form-data-after-save issue in both settings forms), and delivered the full report above. That work is done.
2026-09-06 12:17:10 +01:00
Stefan Lazic 2d4f9c5bf0 Email Settings
CI / ci (push) Canceled after 0s
1. Architecture chosen
Manager pushes environment variables into every business's .env, rather than businesses fetching config at runtime. Manager has no database of its own today (confirmed via code — no Prisma, no ORM dependency anywhere in manager/); every piece of its state is either an env var or a flat file under STACKS_DIR. A "runtime shared-config source" design would make every single email send (including OTP login) depend on Manager being reachable — a new failure mode this app has never had, for no real benefit. Instead:

Storage: one JSON file, stacks/manager/global-settings.json (gitignored), shaped for future sections (SMS, storage, backups, branding...) to slot in as sibling keys.
Secret at rest: the SMTP password is AES-256-GCM encrypted with a new MANAGER_SETTINGS_ENCRYPTION_KEY, never returned to the browser.
Rollout: saving complete settings automatically pushes the 7 EMAIL_* keys into every business's .env via the exact same surgical upsertEnvValues helper updateBusiness() already uses for name/domain edits, then force-recreates each container (composeUp(dir, {forceRecreate:true})) — the identical mechanism that already applies domain changes today.
New businesses get the current global settings baked into their .env at provisioning time — no manual step, ever.

2. Files changed
New (Manager): global-settings.ts (storage+encryption), email-rollout.ts (push-to-all-businesses), test-email.ts (standalone nodemailer test-send), dashboard/settings/ (page.tsx, actions.ts, email-settings-form.tsx).

Modified (Manager): compose-template.ts (optional email block in generated .env), provisioning.ts (reads global settings at creation), dashboard/layout.tsx (Settings nav link), package.json/package-lock.json (added nodemailer).

Modified (main app): brevo-email-provider.ts (added encryption mode + replyTo, backward-compatible), its test file (8 new tests).

Modified (docs): root .gitignore, .env.example, .env.business.example, manager/.env.example.

3. New volumes/databases/config files
None — no new Docker volume, no database. One new gitignored file, stacks/manager/global-settings.json, living inside the STACKS_DIR bind-mount Manager already has.

4. New environment variables
MANAGER_SETTINGS_ENCRYPTION_KEY (Manager only, required to use Settings) — 32-byte hex, generate with openssl rand -hex 32.
EMAIL_SMTP_ENCRYPTION and EMAIL_REPLY_TO (main app, both optional) — unset EMAIL_SMTP_ENCRYPTION preserves today's exact port-based inference, so existing deployments are unaffected.
Reply-to is now business-specific with global fallback,

5. How settings are persisted
stacks/manager/global-settings.json, written by saveEmailSettings(). The password is encrypted before writing; every other field is plain text (host, port, username, from-address, from-name, reply-to, encryption mode, last-test result) since none of those are secrets.

6. How business containers receive global settings
Via their own .env file (Docker Compose's env_file:, unchanged mechanism) — Manager writes the 7 EMAIL_* keys into it and recreates the container. Nothing in the business app itself needed a new runtime dependency; getEmailProvider() still just reads process.env once at process start, exactly as before.

7. What happens to existing businesses when settings change
Saving complete settings (host+username+password+from-address all present) automatically loops every business, updates its .env, and force-recreates its container in one action — the UI reports "applied to N of M businesses" with per-business failure reasons. A separate "Reapply to all businesses" button retries this without re-entering anything, for recovering from a business that was down during the last save. Saving incomplete settings persists them as a draft and touches no business (verified live — this is what stops a business from being flipped into a broken "brevo with blank credentials" state).

8. Manual deployment/update steps on the VPS
Generate a key: openssl rand -hex 32, add it as MANAGER_SETTINGS_ENCRYPTION_KEY to stacks/manager/.env.
Rebuild/redeploy the Manager image and the main app image (both changed) as usual.
Open Manager → Settings, fill in real SMTP details, send a test email, then Save — this rolls out to every existing business automatically. Nothing else needs to run by hand.

9. Security considerations
Password never logged, never returned to the browser after save (only a hasPassword boolean) — confirmed live: the form re-shows a masked placeholder, not the real value.
Test-email errors are categorized (auth/connection/timeout) before display — the raw SMTP error is never forwarded.
Encrypted at rest with AES-256-GCM; losing/rotating the key makes the stored password undecryptable (documented in manager/.env.example).
Settings routes sit under manager/src/proxy.ts's existing auth gate — no new auth surface.
Validated before rollout: malformed addresses/ports are rejected before Save, and rollout only fires when configuration is actually complete.

On JSON vs. database for Manager: JSON is the right call here. Reasoning:

Manager has zero DB dependencies today, deliberately — its own package.json description says an earlier attempt at giving it business-data access "repeatedly broke." Introducing SQLite/Prisma just for one settings blob breaks that design principle for not much gain.

The access pattern is a poor fit for a DB anyway: read on page-load/provisioning/rollout, written by a human clicking Save occasionally. No queries, no joins, no need for transactions beyond "read-modify-write one small file."

Reconsider if: multiple admins need to edit concurrently with conflict detection, you want an audit history of who-changed-what-when beyond "last test result," or the settings graph grows relational (e.g., per-business overrides tied by foreign key to businesses) rather than a flat global blob. None of that applies yet.
2026-09-06 10:38:21 +01:00
Stefan Lazic 973187dcc7 Multi-administrator support
CI / ci (push) Canceled after 0s
1. Summary of changes
The installer-created account is no longer special. "Owner" is now literally renamed Administrator and behaves exactly like the existing role/permission system already allowed — the codebase turned out to already define "administrator" structurally (a StaffProfile whose role holds MANAGE_STAFF), not as a name or a business.ownerId-style field, and last-administrator protection already existed. The real work was: closing the gap between that and a full admin-management UI, fixing two OTP-migration bugs the new spec's requirements exposed, and making the existing protection transactional.

Businesses can now have any number of Administrators, added via Invite Administrator (name + email → OTP-verified before access), promoted/demoted via the existing staff role dropdown (now intercepted with a confirmation dialog when it crosses the Administrator boundary), or removed via the existing deactivate flow.
Two real bugs found and fixed, both required for the spec's "unverified email must not get access":
Invitation acceptance (/invite/accept) never actually verified an OTP — it silently relied on a sign-in path that would have failed for every real invitee. It now sends and verifies a real code, reusing the shared OtpCodeStep UI.
A newly-invited (not-yet-accepted) account could sign in via plain email OTP before ever touching the invite link, because sign-in only checked "does a User row exist," not whether it was verified. Fixed in the shared OTP service, benefiting every invitation, not just administrators'.
Last-administrator protection (already existed) is now transactional — a SELECT ... FOR UPDATE lock on the business row closes the race where two concurrent removals of two different administrators could both succeed.
Administrators (any staff member, in fact) can now change their own email — new address must be OTP-verified before it replaces the login email.
No hidden installer privilege: confirmed via code audit and a live install → the account is a plain Administrator-role staff member, provable identically via the Team UI as any later-added one.
NextChair Manager and platform Super Administrator are untouched.
2. Prisma/database changes
No schema changes. One data-only migration: prisma/migrations/20260905214944_administrator_role_rename — UPDATE Role SET name = 'Administrator' WHERE name = 'Owner' AND isSystem = true. Runs through the normal migration-deploy process; not something to run by hand.

3. How Administrator roles are represented
A per-business Role row named "Administrator" (was "Owner"), isSystem: true, holding every Permission — created identically at install and in dev seed data. This is what "Invite"/"Make Administrator" assign.
Whether a StaffProfile currently counts as an Administrator is permission-based (holds MANAGE_STAFF), not a name check — the same definition the codebase already used for last-owner protection (isStaffAdministrator in staff.service.ts). This is deliberately reused rather than introduced, and stays correct if a business ever gets a custom role that also grants MANAGE_STAFF.
No Business.adminId-style field anywhere — confirmed none existed before this work either.
4. How administrator invitations work
Reuses the existing StaffInvitation model and createStaffProfile/createInvitation machinery verbatim — a new inviteAdministratorAction just pre-selects the Administrator role and uses a minimal Name+Email form (invite-administrator-form.tsx). Acceptance (accept-invitation.ts) now requires a real OTP code sent to the invited email before the account activates — expiry (7 days), resend, and revoke are all the pre-existing invitation mechanics (revoke = the existing deactivate action, relabeled "Revoke Invitation" while pending).

5. How last-administrator protection is enforced
Server-side only, inside staff.service.ts's updateStaffAsOwner (role change) and deactivateStaff (remove), each wrapped in prisma.$transaction that first locks the business row (SELECT ... FOR UPDATE), then counts other active Administrators before allowing the change. Verified with a genuine concurrency test (two simultaneous removals against a 2-admin business) and manually in-browser (see below).

6. Installation flow changes
Only the role name and one line of wizard copy changed ("This becomes the first Administrator..."). No structural UX change — still collects name/email, verifies via OTP, creates/activates the account, assigns the role, exactly as before.

7. Files changed
New: the migration; email-change.service.ts + change-email.ts action + change-email-section.tsx; invite-administrator-form.tsx + add-administrator/page.tsx; 3 new integration test files.

Modified — core: staff.service.ts (transactional protection, isStaffAdministrator), staff-invitation.service.ts, otp.service.ts (onlyIfUserExists fix, email-change purpose), default-permissions.ts, permissions.ts, auth.ts (JWT email-refresh, simplified finalize path).

Modified — actions/UI: owner/actions/staff.ts, staff-detail-panel.tsx, team/page.tsx, add-staff-form.tsx, accept-invitation.ts/form/page, otp-code-step.tsx, profile-form.tsx, installer files, seed.ts, plus a terminology sweep ("owner" → "administrator" copy in breaks.ts/time-off.ts/staff-emails.ts/profile page).

8. Manual testing steps
Sign in as the seeded owner → /owner/team shows an Administrator badge and an "Invite Administrator" icon next to "+". Invite someone, accept via /invite/accept?token=... printed to the terminal (code also prints there) — they land signed in with full Manage access. Promote/demote any staff member via the role dropdown on their detail page — crossing the Administrator boundary shows a confirmation dialog. With only one Administrator left, attempting to demote or deactivate them shows "You can't remove the only administrator. Add another administrator first." Change your own email from Profile settings — the new address must be OTP-verified before it replaces the old one.

9. Deployment steps on the VPS
None beyond the normal deploy process — the migration is a standard Prisma migration that prisma migrate deploy (whatever already runs it there) will pick up automatically. No new environment variables. No changes to NextChair Manager or its authentication.

Verification performed: tsc/lint/unit — all clean; full test:integration — 219 passed (up from 207, all new); full test:e2e — same 36/37 as before this work (the 1 failure is a pre-existing, auth-unrelated flake unrelated to any of this); a full live manual pass covering every scenario above, including the last-administrator block and the email-change JWT-refresh behavior.
2026-09-05 23:32:54 +01:00
Stefan Lazic 94aa1b2f26 OPT and Improved Installation Wizzard
CI / ci (push) Canceled after 0s
1. Summary of changes
Password auth → email OTP, everywhere it existed for the tenant/business app: sign-in, customer self-registration (/register), staff-invitation acceptance, and the installer's administrator step. Flow is always: email → 6-digit code emailed → code entered → session created, via one shared service (otp.service.ts) reused by every entry point.
NextChair Manager's own auth was not touched — this only affects tenant-side (business admin/staff/customer) authentication, per the spec's explicit instruction.
Installer improvements: Step 2 (Administrator) now asks only Name + Email + optional Phone, verified inline via the same OTP widget (no redirect). Step 3 (Business Details) pre-fills Business Name from the Manager-provisioned BUSINESS_NAME, shows the booking URL (NEXT_PUBLIC_APP_URL) as a read-only info box instead of asking for it, and has a distinctly-labeled "Business Website (optional)" field with copy clarifying it's not the booking URL.
Installation cannot complete with an unverified admin email — enforced server-side (finalizeOtp runs inside the same DB transaction as user/business creation), not just trusted from client state. A mid-wizard refresh still can't create duplicate admins or partial records — that property was preserved by design, not incidentally.
2. Prisma/database changes
New EmailOtp model (email as PK — one row per address, so a new request naturally invalidates the previous code): codeHash, purpose, expiresAt, attempts, verifiedAt, consumedAt, timestamps. Only a bcrypt hash of the code is ever stored.
User.passwordHash — unchanged schema (already nullable), just annotated as deprecated. No destructive migration; historical rows are untouched.
Migration: prisma/migrations/20260905190022_email_otp_authentication — adds EmailOtp only. Generated and applied against throwaway containers only; the real next-chair-mysql production database was never touched (confirmed not even running on this host during this session).
3. New environment variables
None required for production. One new variable, E2E_TEST_MODE, exists purely for the Playwright suite (set only in playwright.config.ts's webServer.env) — it unlocks a /api/test/otp-code debug endpoint and bypasses OTP rate limiting so the same seeded users can sign in dozens of times across one test run. It must never be set on a real deployment; nothing in the app sets it anywhere else.

4. Email delivery configuration
Nothing new to configure. OTP emails reuse the existing EmailProvider abstraction and EMAIL_PROVIDER env var exactly as configured today — console in dev (prints to server logs), brevo in production via the same SMTP credentials already documented. The OTP email is a new template (otp-email.ts) sent directly (bypassing the async notification/cron pipeline, since a 10-minute-expiry code can't wait for a delivery retry loop).

5. Migration considerations for existing users/installations
Existing users lose password sign-in — the only way to authenticate now is email OTP. There is no in-app fallback; this is an intentional, sharp cutover. If existing customers/staff/admins need advance notice, that's a business/communications decision outside this change.
No account data is deleted or migrated — passwordHash rows are simply never read for auth anymore.
prisma/seed.ts / scripts/ensure-super-admin.ts still set a passwordHash for seeded dev users — harmless and vestigial now, not cleaned up (flagging it rather than touching seed data silently).
Installations already completed are unaffected; only new installer runs go through the OTP admin-verification step.
6. Files changed
New:

src/lib/auth/otp.service.ts — the core requestOtp/verifyOtp/finalizeOtp service
src/lib/auth/__tests__/otp.service.integration.test.ts
src/services/notifications/templates/otp-email.ts
src/features/auth/components/otp-code-step.tsx — shared code-entry UI
src/features/auth/actions/request-sign-in-otp.ts
src/app/api/test/otp-code/route.ts — e2e-only, gated on E2E_TEST_MODE
prisma/migrations/20260905190022_email_otp_authentication/
Modified — auth core: prisma/schema.prisma, src/auth.ts, src/features/auth/schemas.ts

Modified — sign-in/register/invitations: sign-in-form.tsx, register.ts, register-form.tsx, accept-invitation.ts, accept-invitation-form.tsx, staff-invitation.service.ts

Modified — installer: installer.service.ts, schema.ts, install.ts, install-wizard.tsx, install/page.tsx, installer.service.integration.test.ts

Modified — email / e2e infra: console-email-provider.ts, playwright.config.ts, e2e/helpers.ts, and the four e2e/*.spec.ts files (dropped the now-unused password argument from every signIn() call).

7. Verification performed
npx tsc --noEmit, npm run lint, npm test (276 unit) — all clean.
npm run test:integration (207 tests, throwaway DB) — includes 11 new OTP-service tests and a rewritten installer suite; a genuine race condition in requestOtp (concurrent requests for a brand-new email double-hitting the upsert's create branch) was caught and fixed here.
Full npm run test:e2e against a fresh throwaway database: 36/37 passed. The one failure (owner-journeys "opening hours conflict" test) is a pre-existing, auth-unrelated ordering flake — appointment-count ambiguity from earlier tests' bookings — not something this change touches or introduced.
Live manual verification in-browser: existing seeded customer sign-in via OTP; new customer registration via OTP (session created, landed on Home); installer wizard end-to-end through Step 3, confirming email-verification gating, Business Name pre-population from BUSINESS_NAME, and the read-only Booking URL / distinct Business Website fields.
All throwaway Docker containers/DBs used for this work have been removed; the real production database was never touched.
2026-09-05 21:03:36 +01:00
Stefan Lazic ba46334179 Delete Business
CI / ci (push) Canceled after 0s
A Delete button now sits next to Edit on each business's detail page, styled in red to distinguish it from the safe actions.
It opens a dedicated confirmation page (matching the Add/Edit page pattern) that clearly lists exactly what gets destroyed — container + media volume, the database by name, the Caddy site/domain, and the stack directory — and requires typing the business's exact slug before the Delete button even becomes clickable. This is a stronger guard than a plain "are you sure?" popup, given what we just learned about how easy it is to lose data here.
deleteBusiness() in provisioning.ts reuses the same primitives as create/edit (composeDown, reloadCaddy, the shared MySQL root connection), and is deliberately best-effort past the first step — if a piece is already gone (matching exactly the kind of half-broken state we just fixed for blake-heritage), it still finishes removing the rest and reports what it couldn't clean up automatically, rather than getting stuck.
Verified live: provisioned a real test business, confirmed its container/database/user/volume/Caddy site all existed, deleted it, and confirmed every single one was actually gone — including checking Caddy's running config (not just the file on disk) to prove the reload took effect. Also confirmed the confirm-button correctly stays disabled for a wrong slug and only enables on an exact match. Your real production database was never touched throughout.
2026-09-05 17:41:38 +01:00
Stefan Lazic 2e739c9a26 Update provisioning.ts
CI / ci (push) Canceled after 0s
2026-09-05 17:14:39 +01:00
Stefan Lazic 3b07124d7e Edit Business flow
CI / ci (push) Canceled after 0s
New files:

manager/src/lib/env-file.ts — a shared, surgical .env reader/updater. It only ever touches the specific keys you tell it to (never regenerates the file), so anything hand-edited into a business's .env since provisioning is preserved exactly.
manager/src/app/dashboard/[slug]/edit/ — the edit page, its form component, and its server action.

Modified:

provisioning.ts gained updateBusiness() alongside the existing provisionBusiness(), reusing the same Caddy-site-writing and container-recreation logic (refactored into a shared writeCaddySiteAndReload helper).
stacks.ts/compose-template.ts — extended to track a BUSINESS_NAME field (previously collected on the Add Business form but silently discarded — that gap is now fixed too, so newly created businesses get their name persisted from the start).
The detail page and dashboard list now show the business name, and the detail page has an Edit button.

Per the requirements:

Slug is read-only in the edit form (disabled input, never accepted by the server action — it's bound in from the route, not form data).
Domain uniqueness is validated against every other business (and correctly self-excludes the business being edited).
A domain change rewrites the Caddy site file, reloads Caddy, and force-recreates the container; a name-only change does neither.
No secrets (DATABASE_PASSWORD, AUTH_SECRET, etc.) are ever read into the edit UI — only name/domain/databaseName/status are exposed.
Clear success banners ("Changes saved" vs. "...the container was restarted...") and inline error display, matching the existing Add Business pattern.
2026-09-05 17:05:33 +01:00
Stefan Lazic c92ee46d76 Delete compose.yaml
CI / ci (push) Canceled after 0s
2026-09-05 14:03:15 +01:00
Stefan Lazic a0d39026c0 Management UI
CI / ci (push) Canceled after 0s
Attempt 2 of a management ui.
2026-09-05 13:39:55 +01:00
Stefan Lazic 9fe5b4f038 Revert "Update README.md"
CI / ci (push) Canceled after 0s
This reverts commit 5472db320e.
2026-09-05 09:32:37 +01:00
Stefan Lazic 89bce03ebb Revert "Deployment Redesign"
This reverts commit 764d087eac.
2026-09-05 09:32:34 +01:00
Stefan Lazic e5668b3f23 Revert "initial NextChair Manager."
This reverts commit b219fb4519.
2026-09-05 09:32:32 +01:00
Stefan Lazic eaa0d2e513 Revert "Build failure fix"
This reverts commit 7f774e95bb.
2026-09-05 09:32:30 +01:00
Stefan Lazic 7e70fa0583 Revert "Fixed the Manager deployment database dependency."
This reverts commit a8cdc2671e.
2026-09-05 09:32:28 +01:00
Stefan Lazic d063918aae Revert "Build faliure fix 2"
This reverts commit 69bc0adf29.
2026-09-05 09:32:26 +01:00
Stefan Lazic 3e9fe60faf Revert "Fixed the remaining Manager runtime coupling."
This reverts commit bc45608023.
2026-09-05 09:32:23 +01:00
Stefan Lazic 65d2c7322b Revert "Request fix"
This reverts commit d9a253a974.
2026-09-05 09:32:21 +01:00
Stefan Lazic 638b851fce Revert "root-route separation."
This reverts commit 2da916a4c6.
2026-09-05 09:32:20 +01:00
Stefan Lazic c237b565bd Revert "Auth fix"
This reverts commit 1c281c9f5d.
2026-09-05 09:32:18 +01:00
Stefan Lazic 32e93c6c23 Revert "Update management-auth.ts"
This reverts commit 0dd50c0375.
2026-09-05 09:32:16 +01:00
Stefan Lazic 3cef279d2d Revert "Redirect loop fix"
This reverts commit 8fddde6d4b.
2026-09-05 09:32:14 +01:00
Stefan Lazic bcf7fa58d3 Revert "cookie handling"
This reverts commit ac9791c8ef.
2026-09-05 09:32:12 +01:00
Stefan Lazic 3de53f0a67 Revert "Update management-auth.ts"
This reverts commit a65360b82d.
2026-09-05 09:32:10 +01:00
Stefan Lazic 344c08b840 Revert "SSR Leak fix"
This reverts commit 8a2b211069.
2026-09-05 09:32:08 +01:00
Stefan Lazic 2aa8405006 Revert "PHP Rebuild"
This reverts commit 2ef3654841.
2026-09-05 09:32:05 +01:00
Stefan Lazic a59f998f2b Revert "Docker"
This reverts commit c4325018e0.
2026-09-05 09:32:03 +01:00
Stefan Lazic 39eeb937de Revert "Update 2026_09_04_000300_create_nextchair_platform_tables.php"
This reverts commit cd6abeca6e.
2026-09-05 09:32:01 +01:00
Stefan Lazic 33fb14b773 Revert "Management UI"
This reverts commit f11404c574.
2026-09-05 09:31:59 +01:00
Stefan Lazic 3e052e56b1 Revert "Login Loop"
This reverts commit 16c6c28f07.
2026-09-05 09:31:50 +01:00
Stefan Lazic fa96fe0e15 Revert "Update entrypoint.sh"
This reverts commit 5972d37425.
2026-09-05 09:31:41 +01:00
Stefan Lazic 5972d37425 Update entrypoint.sh
CI / ci (push) Canceled after 0s
2026-09-05 09:25:46 +01:00
Stefan Lazic 16c6c28f07 Login Loop
CI / ci (push) Canceled after 0s
2026-09-05 09:24:09 +01:00
Stefan Lazic f11404c574 Management UI
CI / ci (push) Canceled after 0s
2026-09-05 09:11:24 +01:00
Stefan Lazic cd6abeca6e Update 2026_09_04_000300_create_nextchair_platform_tables.php
CI / ci (push) Canceled after 0s
2026-09-05 08:55:12 +01:00
Stefan Lazic c4325018e0 Docker
CI / ci (push) Canceled after 0s
2026-09-05 08:36:54 +01:00
Stefan Lazic 2ef3654841 PHP Rebuild
CI / ci (push) Canceled after 0s
Initial PHP rebuild of the app
2026-09-05 08:21:33 +01:00
Stefan Lazic 8a2b211069 SSR Leak fix
CI / ci (push) Canceled after 0s
2026-09-04 23:02:27 +01:00
Stefan Lazic a65360b82d Update management-auth.ts
CI / ci (push) Canceled after 0s
2026-09-04 22:55:53 +01:00
Stefan Lazic ac9791c8ef cookie handling
CI / ci (push) Canceled after 0s
2026-09-04 22:53:35 +01:00
Stefan Lazic 8fddde6d4b Redirect loop fix
CI / ci (push) Canceled after 0s
The redirect loop was caused by the Manager cookie configuration:
1. secure was based on NODE_ENV=production, so HTTP deployments silently dropped the cookie.
2. Path=/manager excluded /api/manager/*, preventing authenticated Manager API requests.
2026-09-04 22:48:22 +01:00
Stefan Lazic 0dd50c0375 Update management-auth.ts
CI / ci (push) Canceled after 0s
2026-09-04 22:42:28 +01:00
Stefan Lazic 1c281c9f5d Auth fix
CI / ci (push) Canceled after 0s
Implemented
- Root cause fixed in [src/proxy.ts]: Auth.js is now initialized lazily and never invoked when MANAGER_MODE=true.
- Manager requests call handleProxyRequest directly, preserving redirects and CSP.
- Business requests continue through Auth.js with existing protected-route behavior.
- Added coverage in [proxy-manager-mode.test.ts]
2026-09-04 22:28:49 +01:00
Stefan Lazic 2da916a4c6 root-route separation.
CI / ci (push) Canceled after 0s
2026-09-04 22:11:23 +01:00
Stefan Lazic d9a253a974 Request fix
CI / ci (push) Canceled after 0s
- Auth.js handlers require NextRequest.
- The wrapper accepted plain Request.
Changed both GET and POST handlers to accept NextRequest.
2026-09-04 21:59:44 +01:00
Stefan Lazic bc45608023 Fixed the remaining Manager runtime coupling.
CI / ci (push) Canceled after 0s
1. Root cause of the database error
The Manager proxy was bypassing installation checks, but / still resolved to src/app/page.tsx, which statically imported:
- @/auth
- Prisma
- business branding
- business permissions
- primary business data
The root route now redirects to /manager inside the proxy before the business homepage is evaluated.
2. Root cause of the Auth.js error
The global business SessionProvider was mounted around Manager pages. It automatically requested:

The business homepage is not evaluated first.
5. Provider and authentication separation
Manager mode:
- Uses ManagerProviders
- Uses the signed Manager cookie session
- Does not mount SessionProvider
- Does not call /api/auth/session
- Does not import business Auth.js or Prisma
Business mode:
- Dynamically loads the existing business Providers
- Continues using Auth.js and SessionProvider
- Preserves installation, /owner, and /staff behavior
6. Database independence
Manager mode now avoids database initialization for:
- Proxy requests
- Root metadata and viewport
- Root routing
- Manager pages
- Manager APIs
- Manifest requests
- Health requests
- Business Auth.js requests
- Business cron requests
No fake DATABASE_* variables were added.
7. Tests
Added coverage for:
- Manager / routing
- Manager /manager access
- No installation-state check in Manager mode
- Prisma import protection
- Business installation checks
- /owner protection
- Business Auth.js 404 boundary
- Manager health route without Prisma
- Manager manifest without business branding
8. Validation
2026-09-04 21:57:47 +01:00
Stefan Lazic 69bc0adf29 Build faliure fix 2
CI / ci (push) Canceled after 0s
Fixed the new build failure in [src/proxy.ts]
2026-09-04 21:45:54 +01:00
Stefan Lazic a8cdc2671e Fixed the Manager deployment database dependency.
CI / ci (push) Canceled after 0s
Root cause
src/proxy.ts statically imported installation-state.ts, which statically imported Prisma. Additionally, the root layout statically imported business branding, which also loads Prisma. The pathname check was also incorrect for Manager deployments served at /.
2026-09-04 21:43:26 +01:00
Stefan Lazic 7f774e95bb Build failure fix
CI / ci (push) Canceled after 0s
2026-09-04 21:22:33 +01:00
Stefan Lazic b219fb4519 initial NextChair Manager.
CI / ci (push) Canceled after 0s
Key additions:
* Separate platform Manager stack: [stacks/manager/compose.yaml]
* Dedicated manager authentication with signed sessions.
* Business overview dashboard.
* Add Business workflow with slug suggestions and deployment progress.
* Business detail page with start, stop, restart, update, logs, Dockge, and conservative removal actions.
* Explicit management API endpoints under [src/app/api/manager]
* Shared provisioning service layer in [provisioning.service.ts]
* CLI and UI now use the same [create-business.sh] provisioning logic.
* Registry metadata, database status, container health, and media usage reporting.
* Manager-only entrypoint mode that does not require a business database.
* Updated deployment documentation with Manager setup and Caddy routing.
Validation passed:
* Infra, Caddy, Manager, and generated business Compose configurations.
* Shell syntax checks.
* Registry generation with registry image tags.
* git diff --check.
2026-09-04 20:54:58 +01:00
Stefan Lazic 764d087eac Deployment Redesign
CI / ci (push) Canceled after 0s
Implemented the deployment redesign.
- Added Dockge-first-class stacks:
  - [stacks/infra/compose.yaml]
  - [stacks/caddy/compose.yaml]
- Added reusable business template: [deploy/business-compose.yaml.template]
- Added automated provisioning: [scripts/create-business.sh]
- Added per-business database/user, secrets, media volume, private networks, and Caddy route generation.
- Removed Traefik and manual business Compose files.
- Updated environment templates, .gitignore, and deployment documentation in [README.md]
Validation completed:
- Docker Compose config passed for infra, Caddy, and generated business stacks.
- Shell syntax validation passed.
- Registry image tags containing / were tested.
- No remaining Traefik or obsolete deployment references found.
2026-09-04 20:32:06 +01:00
Stefan Lazic 5472db320e Update README.md
CI / ci (push) Canceled after 0s
2026-09-04 19:02:02 +01:00
Stefan Lazic 841ee7eb52 VPS
Example of vps deploy with infra split from business.

Keeps one mysql instance for all.
2026-08-07 19:45:56 +01:00
Stefan Lazic b41a6d0af7 Create route.ts 2026-08-07 18:06:39 +01:00
Stefan Lazic 9949e2275e Database 2
fix for vercel build
2026-08-07 17:39:31 +01:00
Stefan Lazic 51de4f4ef5 Database
Updated to just use DATABASE_* in everything other than local docker.
2026-08-07 17:30:12 +01:00
Stefan Lazic 769d3f9ebc PWA Settings
Fixed webmanifest generation and pwa icon.

Made nav bar text optional.
2026-08-07 13:38:11 +01:00
Stefan Lazic a702320d4e UI Updates
Updated some pages pages to be more consistent.
2026-08-06 19:21:31 +01:00
Stefan Lazic 90ec663e6e Staf Booking
fixed staff ability to book a customer in
2026-08-06 14:49:46 +01:00
Stefan Lazic 973f18f64c Customer Profile
Customer profile update with image upload.
2026-08-06 12:11:36 +01:00
Stefan Lazic f07c30da0d Reviews & Layout
Scaled back google reviews to just get the star rating and number of reviews as google limits the number of reviews to 5.

Also removed the reviews tab from homepage.

And updated Appointments button layout
2026-08-06 09:57:24 +01:00
Stefan Lazic 92f062a861 Layout Changes
Small tweaks to the layout
2026-08-05 22:23:14 +01:00
Stefan Lazic 3300045d9e Staff / Owner
Merged staff and owner settings
2026-08-05 16:35:52 +01:00
Stefan Lazic cf3a28f3cd Loyalty Card updates
1. Card title fix: The "top-right text" feature was renamed and repositioned to be the actual card title (top-left, replacing the hardcoded "Loyalty Card"), matching what you asked for. This included a proper database migration (cardTopRightText → cardTitleText, etc., using CHANGE COLUMN so any already-configured value is preserved, not dropped) and updates through every layer — schema, service, owner form, and both the owner preview and customer-facing card. Verified live: typed "VIP Rewards" into the new "Card title" field and confirmed it renders top-left on both the owner's live preview and the real customer loyalty page, with the member number staying in its own badge top-right.

2. Tabs: The long "Card Appearance" control list is now split into Colours / Stamp / Text tabs (using the same Tabs component used elsewhere in the app), exactly as you suggested — Colours holds every colour + the background image, Stamp holds just the icon picker, Text holds the title, font, size, and terms line.

Along the way, while running full E2E regression, I found 6 test failures — but traced them via git stash to prove they're pre-existing bugs unrelated to this change (a booking/appointments-list bug), so I reverted-then-confirmed before restoring my work, and filed a separate follow-up task for them rather than mixing it into this change.

Both fixes are typechecked, linted, verified with the full unit (253) and integration (185) suites, confirmed live in a real browser end-to-end, and deployed to your running Docker container — the migration applied cleanly against your real database with no data loss.
2026-08-05 12:19:08 +01:00
Stefan Lazic d3bbad09c6 Fixes
Fixed an issue where media manager only shows files uploaded in specific sections.

Fixed an issue with icon picker names.

Removed jumbo rounding and a few other style tweaks.
2026-08-05 09:25:59 +01:00
Stefan Lazic 9e39908ea7 Post Phase 8 - Fixes 1
Delivered:

Super Administrator — env-var provisioned, no StaffProfile at all (structural exclusion from Team/barber-picker/appointments/staff-totals, not a filter). hasPermission short-circuit + new getActingBusinessContext() helper retrofitted across 16 owner pages so it can actually manage the business.
Media Manager — one upload/library/external-URL modal + field picker, backing every image field app-wide. Local-disk storage + Docker volume, SVG rejected, size-capped.
Welcome backgrounds — desktop/mobile images with CSS breakpoint swap.
Business Branding settings — logo/cover/welcome/favicon/icon/colours all editable post-install; cover image finally rendered (was dead config).
Service descriptions — now surfaced on Home and in the booking flow.
Loyalty card — live preview, ColorPicker/MediaPicker/IconPicker, flip removed for a static premium-styled card, Loyalty added to bottom nav. Fixed a real pre-existing bug where the card's configured colour/image/icon were stored but never actually rendered.
Home Screen Redesign — hero gallery + sticky Services/Portfolio/Reviews/Details tabs, backed by a new unified gallery model/service (no hero/portfolio duplication). Details tab surfaces social links and a map that were never shown before.
Google Reviews — real sync pipeline (24h cron, CRON_SECRET-gated, mirrors the existing notification cron exactly), DB-only customer reads, owner hide/unhide + policy management page.
Architecture cleanup — SettingsPageShell extracted from 6 duplicated page wrappers; fixed a second pre-existing bug where branding image fields could never actually be cleared once set.
Verification: 418 tests passing (236 unit + 182 integration, all against a real MySQL database), full typecheck/lint clean across src, and a real Docker build+run cycle — which caught and fixed a genuine bug (scripts/ wasn't copied into the production image, so Super Admin sync would have crashed on every real deployment).
2026-08-04 17:10:30 +01:00
Stefan Lazic 77f25c0489 Update button.tsx 2026-08-04 14:36:14 +01:00
Stefan Lazic 4eb93411bb Update proxy.ts 2026-08-04 10:13:10 +01:00
Stefan Lazic 4785510144 Phase 8 2026-08-04 09:57:58 +01:00
Stefan Lazic 546613e677 Phase 7 2026-08-03 21:15:24 +01:00
Stefan Lazic ec5583f972 Phase 6
Owner Administration is complete. Summary of this final stretch of work:

Seed data extended with an inactive staff member, a pending invitation, a redeemed-reward fixture, a service with no eligible staff, and a future-appointment fixture for conflict testing — all idempotent, verified against the dev DB.

Testing brought to 179 unit / 86 integration / 27 E2E tests, all passing together on a freshly seeded, disposable Postgres:

Unit tests for every new pure validator (business hours, categories/services, booking rules, loyalty config, staff creation).
Integration tests covering opening-hours conflicts, last-owner protection, staff deactivation/eligibility conflicts, loyalty redemption's atomic race-guard, and owner-vs-barber/business-isolation authorization — all against a real Postgres.
12 new Playwright owner journeys, which surfaced and fixed two genuine issues along the way: a cookie-clearing race condition in the sign-out-mid-test flow (a real, reproducible timing bug, not just a test artifact), and confirmation that seed fixtures upserted by deterministic ID go stale once "today" has moved on — now handled defensively.
Desktop responsiveness: converted the single-column Business Details, Booking Rules, and Loyalty Configuration forms to a genuine two-column layout at the lg: breakpoint (verified via measured element positions, not just screenshots), confirmed mobile stacking is unaffected.

Final verification: clean tsc, eslint, and next build, plus the full three-suite run against one fresh database as the authoritative pass.
2026-08-03 12:56:55 +01:00
Stefan Lazic ffee9d6754 Phase 5
Phase 5 is complete. All 53 tasks are done, verified on a fresh database in a single clean run: **121 unit tests**, **55 integration tests**, and **15 E2E journeys** (6 Phase 4 + 9 new Phase 5) all passing, plus a clean `tsc`/`eslint`/`build`.

Summary of this final stretch:

- **Fixed a real bug**: `StaffService`'s break/time-off update/delete functions threw uncaught instead of failing gracefully on a permission mismatch — fixed with proper `PERMISSION_DENIED` handling, covered by new integration tests.
- **Built the remaining Phase 5 UI**: Breaks editor, Time Off editor, Barber Profile, and Notification Preferences — all wired to real services, no placeholders.
- **Extended seed data**: more customers, loyalty accounts/transactions/rewards, and appointments spanning every status today (anchored to real time so the "Current Appointment" priority logic is reliably exercised).
- **Found and fixed a second real bug live in the browser**: the Break/Time-Off edit sheets showed stale default values instead of the item being edited (a classic `useState`-doesn't-reinitialize-on-prop-change bug) — fixed with a `key` prop to force remount.
- **Wrote the full Phase 5 test suite**: unit tests for `selectCurrentAppointment` and all `StaffService` validators, integration tests for `LoyaltyService` and `StaffService` (including the permission-fix regression tests), and 9 new Playwright E2E journeys covering the brief's exact required list.
- **Did the desktop-responsive pass**: a persistent sidebar nav replacing the bottom bar at `lg:`, a genuine calendar+"Right Now" side-by-side split, and 2-column layouts for the clients list and customer details — addressing the brief's explicit "more than simple centering" requirement.
- **Wrote [docs/phase-5-summary.md](docs/phase-5-summary.md)** documenting everything above in full.

Nothing is committed — the whole session (Phases 2–5) sits uncommitted on top of the initial template commit, same as you left it. Let me know if you'd like me to commit, or if you want to move on to Phase 6.
2026-08-02 23:44:18 +01:00
Stefan Lazic 6ae145d187 Phase 4
Seed data extended with a second barber (different hours/services), a beard-trim service + category, breaks, time off, a guest customer, and appointments in all three key states — verified against a live throwaway Postgres.
14 new integration tests (38 total with Phase 3's) covering guest/authenticated booking, any-barber assignment, slot-conflict handling, ownership enforcement, guest-data isolation, and book-again repricing.
Playwright E2E suite for the 6 required customer journeys, with its own global-setup.ts that migrates and seeds a database automatically.
Full verification pass, which caught and fixed three real bugs along the way (not just test-workarounds):
A date off-by-one bug in the calendar picker that made real weekdays show as closed for anyone in a UTC+ timezone (including the business's own).
A dialog that could vanish mid-confirmation on cancellation, before the customer could read the late-cancellation notice.
A silent test-cleanup leak in the Phase 3 fixtures that had been leaking stray businesses since Phase 3.
Plus a baseline desktop-responsive fix (centered max-w-2xl columns) across Home/Appointments/Settings/booking flow, since desktop had been effectively unstyled.
Final state: 90 unit tests, 38 integration tests, and all 6 E2E journeys passing together on a fresh database; clean tsc, eslint, and next build. Full writeup with routes, security model, guest-matching rules, known limitations, and what to check before Phase 5 is in docs/phase-4-summary.md.
2026-08-02 21:39:24 +01:00
Stefan Lazic 8b63cc66a9 Booking Engine
Architecture: Six focused services with the BookingService/SchedulingService overlap resolved by giving SchedulingService exactly one job, the transactional double-booking guard , while BookingService owns the create/reschedule/cancel workflow around it.

59 unit tests, 24 integration tests
2026-08-02 18:36:39 +01:00
Stefan Lazic 090646fdfd Phase 2
Base app architecture:

    Route groups for customer, staff and owner.
* Reusable design system with around 20 UI components.
* Complete Prisma schema.
* Authentication with email/password and Google.
* Role and permission model.
* Protected routes.
* Clean separation between features, services, components and shared utilities.
2026-08-02 17:47:23 +01:00