12 Commits

Author SHA1 Message Date
belviskhoremk
a5b210f309 feat: add site_content, testimonials, and rules tables; featured products 2026-08-13 08:51:41 +02:00
belviskhoremk
f41c6f5812 feat: replace admin account with admin@braidsbymira.com
Swaps the single fixed admin UUID that every admin-write/admin-read RLS
policy and the frontend's ProtectedRoute check against, moving off the
personal belviskhoremk@gmail.com account onto a dedicated
admin@braidsbymira.com login created via the Auth admin API. Updates
the RLS test suite's admin-impersonation UUID to match.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 23:51:49 +02:00
belviskhoremk
43f4f5f46e fix: block duration-aware slot overlaps, not just exact time matches
booked_slots previously recorded only (date, time), so availability
checking excluded an exact time match but not a real overlap — a
4-hour service booked at 9:00 AM didn't block 11:00 AM or 1:00 PM.
Add duration_minutes to booked_slots (populated by sync_booked_slot()
from the booked service's own duration) and switch client-side
computeAvailableSlots to a half-open-interval overlap check, also
rejecting candidates that wouldn't finish before closing time.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 23:15:05 +02:00
belviskhoremk
3504790216 fix: address final whole-branch review findings for Phase 2B admin panel
Consolidated fix wave for 1 Critical + 6 Important findings from the final
review before merge:

- Critical: marking a booking completed no longer frees its booked_slots
  mirror row or the unique-slot index, closing a double-booking hole
  (new migration 20260812110508, with regression pgTAP tests added first
  and confirmed failing against the old trigger/index).
- uploadSalonImage() now returns the storage-relative path instead of an
  absolute public URL, so products/services.image stays one consistent
  format.
- The product/service Id field is disabled while editing, preventing
  accidental duplicate rows from an upsert keyed on a changed id.
- Cancel / Mark completed in AdminBookings now require window.confirm,
  matching the existing delete-confirmation pattern.
- /admin and any unmatched /admin/* path now redirect to /admin/products
  instead of rendering blank.
- Public signup is disabled in supabase/config.toml, and ProtectedRoute
  adds a defense-in-depth check that the session belongs to the admin
  user id, not just that a session exists.
- Product/service delete now surfaces a friendly message for FK-violation
  errors (23503) via a new friendlyError() helper, instead of a raw
  Postgres error string.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 13:12:41 +02:00
belviskhoremk
71b812d5d4 Merge branch 'master' into worktree-phase2b-admin-panel
Brings in the Phase 2A follow-up fixes (booking-slot uniqueness,
booked_slots cleanup on reschedule/cancel/delete, notification_failures
table, worktree-exclusion hardening for eslint/vitest) so Task 12's
local verification runs against the fully-integrated schema.

Resolved the rls.test.sql conflict by hand: both branches independently
appended tests at the end of the file. Combined both sets (bucket-1's
booking/slot/notification tests + this branch's salon-images/booking-
completed tests) and bumped select plan() from 17/37 to 42, matching
the true combined assertion count.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 11:54:12 +02:00
belviskhoremk
bcc606db28 feat: allow bookings.status to be marked completed 2026-08-12 10:51:17 +02:00
belviskhoremk
acc4b02a20 feat: add public-read, admin-write RLS policies for salon-images bucket
Adds pgTAP tests and migration to create the salon-images storage bucket
with RLS policies allowing public read access and admin-only write access.
Tests verify anon users can read but not upload, and admin can upload.
2026-08-12 10:44:44 +02:00
belviskhoremk
e6061d7ba4 fix: address review — reschedule desync, pg_net comment, self-healing unique index
Fixes 3 Important issues from re-review of the previous fast-follow-up commit:

1. booked_slots desync on reschedule: the cleanup trigger was `after update
   of status`, so changing date/time on a still-confirmed booking never fired
   it -- the OLD slot stayed blocked and the NEW slot stayed shown as free.
   Changed to an unqualified `after update`, and the UPDATE branch now
   compares the full (date, time, status) triple instead of just status, so
   it frees the old slot on any change away from it and (re)mirrors the new
   one when the row is still/now confirmed. Added pgTAP coverage: reschedule
   removes the old slot's mirror row and creates the new one.

2. The Edge Functions' comments/commit message claimed a non-200 response
   would make pg_net "retry" -- that's not how pg_net works, it's
   fire-and-forget and never inspects the response status. Corrected the
   comments to state the real reasoning (nothing consumes the status code,
   so a 500 wouldn't help and would misrepresent an already-successful
   booking/inquiry as failed). Closed the actual gap that created -- a failed
   Resend send was only visible in Edge Function logs nobody reads -- by
   adding a `notification_failures` table (new migration, admin-only select
   via RLS, no anon/authenticated write access, only service_role writes)
   and having both handlers insert a row (booking/inquiry id + error
   message) whenever the Resend call fails, the API key is missing, or an
   unexpected error occurs. Insert failures are swallowed (best-effort --
   already in a failure path, must not throw a louder one). Added pgTAP
   tests for the table's RLS/grant shape.

3. `20260812000002_bookings_slot_uniqueness.sql` could have failed outright
   on `supabase db push` against live data if any existing confirmed booking
   already shared a (date, time) with another. Added
   `dedupe_confirmed_booking_slots()`, called before the index is created:
   it collapses each duplicate group down to the earliest-created row
   (kept 'confirmed', others cancelled) and logs what it did via
   `raise notice`, so index creation is guaranteed to succeed either way.
   Extracted as a standalone, service_role-only-executable function (rather
   than inlining the SQL) specifically so the pgTAP test can exercise the
   exact same logic the migration runs. Added a test that seeds two
   duplicate confirmed bookings (having first dropped the index to simulate
   the pre-migration state), runs the dedupe function, and asserts exactly
   one confirmed booking remains -- the earliest one -- before recreating
   the index.

pgTAP suite grew from 23 to 37 tests. Verified locally: supabase db reset
(all 7 migrations apply cleanly), supabase test db (37/37 pass), vitest run
(51/51 pass), npm run build, npm run lint, and npm run test:functions
(6/6 pass) all succeed. Still nothing here touches the live Supabase
project.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 10:42:25 +02:00
belviskhoremk
c74f1bb6cb fix: prevent double-booking, clean up booked_slots, harden Edge Function error handling
Fast follow-up to the Phase 2A backend review (4 deferred Important items):

1. Add a partial unique index (date, time) where status = 'confirmed' on
   public.bookings so two concurrent bookings for the same slot can no longer
   both succeed. Previously sync_booked_slot()'s `on conflict do nothing`
   masked the second insert as a silent no-op on the mirror table instead of
   rejecting it.

2. Extend sync_booked_slot() (new migration, the applied 20260808000002 is
   left untouched) to also fire on `update of status` and `delete`, removing
   the booked_slots mirror row once no confirmed booking remains for a slot.
   Also tightened the insert path to only mirror bookings that are actually
   'confirmed' -- the original trigger mirrored every insert regardless of
   status, so a booking inserted directly with status='cancelled' (allowed by
   the public insert policy) would incorrectly block a slot nobody confirmed.

3. supabase/functions/{on-booking-created,on-inquiry-created}: guard
   req.json() and payload.record with try/catch (400 + console.error on
   failure), check the Resend response's .ok and log failures instead of
   swallowing them, and skip the Resend call entirely (with a log) instead of
   sending "Authorization: Bearer undefined" when RESEND_API_KEY is unset.
   Still return 200 when only the notification leg fails, since the booking/
   inquiry row itself already committed successfully by the time this handler
   runs -- a non-200 would make pg_net retry, but retrying re-sends against
   the same already-inserted row and won't fix a Resend outage or bad
   payload, so it isn't the right signal.

4. README: document the Supabase-side dev loop (supabase start/db reset/test
   db), npm run seed and its SUPABASE_URL/SUPABASE_SERVICE_ROLE_KEY env vars,
   and npm run test:functions (Deno tests for the Edge Functions' shared
   email builders).

pgTAP suite grew from 12 to 23 tests (11 new: 4 for the uniqueness
constraint, 7 for booked_slots cleanup on cancel/delete). Verified locally:
supabase db reset, supabase test db (23/23 pass), vitest run (51/51 pass),
npm run build, npm run lint, and npm run test:functions (Deno installed for
this session; 6/6 pass) all succeed. Nothing here touches the live Supabase
project -- deploying is a separate follow-up step.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 10:29:37 +02:00
belviskhoremk
8c1522a2b4 chore: deploy Phase 2A backend to live Supabase project (Task 9)
Substitutes the real admin UID everywhere the placeholder appeared,
points the Edge Functions base URL at the deployed project, and adds
production JWT verification: trigger functions now send the anon key
as a bearer token so verify_jwt can stay enabled instead of leaving
the functions open to unauthenticated calls from the public internet.

Also adds scripts/create-admin.ts (one-off helper used to create
Mira's admin auth user), documents the seed script's non-VITE env
vars in .env.example, and closes a gitignore gap where plain .env
wasn't excluded alongside .env.local.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 09:59:33 +02:00
belviskhoremk
aacb9dca12 fix: scope GRANT statements to match actual RLS-permitted access per role
Replace the blanket select/insert/update/delete grant to anon/authenticated/
service_role with privilege-scoped grants: anon/authenticated get select-only
on the public-read tables plus insert-only on bookings/inquiries; full CRUD
is reserved for authenticated (narrowed to the admin UID by RLS) and
service_role. Table grants are now a real second layer of defense instead of
mirroring RLS. Updates two pgTAP assertions whose blocking mechanism moved
from RLS-filtered-empty to grant-level permission-denied, and adds a new
assertion proving anon cannot UPDATE bookings.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-08 13:08:55 +00:00
belviskhoremk
5e3a77fc70 feat: add booked_slots sync trigger and pgTAP RLS/trigger tests
Includes explicit GRANTs for anon/authenticated/service_role on all
public tables: the Supabase CLI's current default no longer
auto-exposes new tables, so RLS policies from the initial schema were
unreachable without them (every request failed at the privilege check
before RLS was evaluated).
2026-08-08 13:01:08 +00:00