Renamed useAdminAuthMock to adminAuthMock in ProtectedRoute.test.tsx to prevent
ESLint from flagging it as an invalid hook call. The 'use' prefix naming heuristic
was causing a react-hooks/rules-of-hooks rule violation, though it was not actually
a hook call context. This change maintains all test behavior while passing lint.
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.
Running the suite from the main checkout was picking up each
worktree's full copy of src/ under .claude/worktrees/, silently
running every test twice. The existing supabase/functions/** exclude
only matched at repo root, so it also missed the nested worktree's
copy of that path.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Fixes issues raised by the whole-branch review before merge:
- Critical: the previous migration hardcoded the live production
functions URL and anon key as the committed default, so every local
`supabase db reset` silently pointed dev/test triggers at the real
project. Adding a new migration instead of editing the already-applied
one (checksum/drift safety), with local dev values as the committed
default; production values are applied out-of-band, never committed.
- Important: verify_jwt=true plus sending the anon key as a bearer
token didn't actually stop anyone, since the anon key is public
(ships in the frontend bundle) — confirmed empirically, a request
with a valid anon key but no shared secret now gets a real 401.
Added a random shared secret known only to the database and the
Edge Functions' secret store.
- Important: notification emails interpolated raw contact fields into
HTML with no escaping. Added escapeHtml() and tests proving injected
markup renders as literal text, not live markup.
- Important: the URL/key/secret functions moved from public to a
private schema with EXECUTE revoked from anon/authenticated, closing
an unintended PostgREST RPC endpoint (verified 404 after the fix).
Also adds a test:functions npm script now that Deno is confirmed
available and the shared email builders' tests pass (12 total incl.
the new escaping cases).
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
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
The brief's literal script copies source fields verbatim, but src/data
uses camelCase (inStock, durationMinutes, priceFrom) while the DB
columns are snake_case (in_stock, duration_minutes, price_from), so
that version fails with PGRST204. Map fields explicitly instead of
spreading the rest.
notify_booking_created() and notify_inquiry_created() each held their own
literal copy of the local Edge Functions base URL. Introduce
public.edge_functions_base_url() as the single source of truth so Task 9's
production deploy only needs to update one function definition instead of
two independent literals.
Wires pg_net-based database triggers on bookings/inquiries to invoke the two
Edge Functions on insert. Two empirically-discovered fixes over the brief's
literal SQL were required to make this work locally:
- ALTER DATABASE ... SET for a custom GUC needs superuser, but the local
migration role (postgres) isn't superuser in this Supabase CLI stack (only
supabase_admin is) - inlined the base URL as a literal constant in each
trigger function instead.
- pg_net's http_post lives in schema `net`, not `extensions.net` - fixed the
function calls accordingly.
Also disabled verify_jwt for both functions in config.toml since they're
invoked by pg_net (no Authorization header), not by end-user clients.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Adds supabase/functions/ (deno.json, _shared/email.ts) with pure
buildBookingNotificationEmail/buildInquiryNotificationEmail functions
for Task 6's Edge Functions, tested via Deno's test runner. Excludes
supabase/functions/** from the root Vitest config, which otherwise
tried to resolve the jsr: import and failed.
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
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).
Initializes the local Supabase project (supabase/config.toml) and adds
a thin src/lib/supabase.ts wrapper around @supabase/supabase-js that
reads connection details from Vite env vars, tested with a mocked
createClient.
Adds emulator-backed rules-unit tests verifying the Firestore rules
committed in Task 3, including the global constraint that
bookings/inquiries are never publicly readable. Excludes
firebase-tests/ from the default vitest run since it needs the
emulator (via `npm run test:rules`) rather than jsdom.
The old flex layout with justify-content: space-between pushed its
two children to the far edges on wide screens, leaving a large dead
void in the middle. Replaced with a centered, max-width three-column
grid (brand, contact, navigate) with a script wordmark, gold section
labels, a divider separating it from the banner above, and a
copyright line.
Sourced from Pexels (free for commercial use, no attribution
required) to complete the image wiring added in the visual overhaul.
Fills the 18 image slots that were previously deferred (products,
signature styles, About Mira portrait, testimonial avatars).
Kept in French to match how Mira actually messages clients. Updated
RuleIcon's icon set to match the real policy categories (deposit,
hair prep, extensions, lateness, guests, payment) in place of the
placeholder categories.
- Lazy-load Home in App.tsx so three/@react-three/fiber (~238 kB gzip) only ships to the
Home route, not to Shop/Book/Rules.
- Add max-width: 260px to ProductCard/ServiceCard so real images won't collapse Home/Book
grid layouts.
- Restore AnimatePresence mode="wait" in Book.tsx to eliminate the exit/enter double-stack
during step transitions, updating the three Book.test.tsx assertions that raced the
transition to findBy* queries.
- Hero3DScene: drop the meaningless "static" className string in favor of a dedicated
data-testid, add aria-hidden to the reduced-motion fallback branch, and remove the
unused @react-three/drei dependency (adding @types/three directly since it was only
present transitively via drei).
Adds StepProgress component showing 4 labeled booking steps (Service,
Date & Time, Contact, Confirmed) and wires it into Book.tsx alongside
framer-motion AnimatePresence/motion.div step transitions.
Deviates from literal brief in two ways, both to avoid known issues:
- Guards the transition with useReducedMotion() (duration: 0 when
prefers-reduced-motion is set), matching the fix pattern already
applied to Home.tsx and Shop.tsx after prior review round-trips.
- Drops AnimatePresence's mode="wait" in favor of the default (sync)
mode: with mode="wait", the incoming step's DOM doesn't mount until
the outgoing step's exit animation finishes, which broke existing
synchronous Testing Library queries in Book.test.tsx (real 250ms
exit animations in jsdom, no fake timers). Sync mode mounts the new
step immediately while both fade, preserving test synchronicity.
Also updates Book.test.tsx's confirmation assertion since the old
"You're booked!" heading is gone (page now always shows the shared
"Book an Appointment" heading and StepProgress; only the body swaps).
The About section's portrait image now checks useReducedMotion() and
disables parallax scroll motion when the user has set OS-level reduced
motion preferences.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Adds Hero3DScene, a procedurally-built rotating braid mesh rendered via
@react-three/fiber, replacing the flat CSS-circle Hero3D component. Falls
back to a static gradient circle under prefers-reduced-motion, matching the
old component's fallback behavior.
Also adds a ResizeObserver mock to the test setup, required by
@react-three/fiber's Canvas (via react-use-measure) in jsdom, which doesn't
implement it natively.