# Phase 2B: Admin Panel — Design **Status:** Approved **Supersedes:** N/A. Builds directly on the Phase 2A Supabase backend (`docs/superpowers/specs/2026-08-08-phase2-backend-foundation-supabase-design.md`), which this spec assumes is complete and deployed. ## Context Phase 2A shipped a Postgres schema (products, services, schedule, bookings, inquiries), RLS policies gating writes to a single fixed admin UUID, and notification Edge Functions — all live on a real Supabase project. That spec explicitly deferred "the admin panel UI itself (login screen, product/service/ availability management screens, bookings/inquiries dashboard)" to a future sub-project, calling it Sub-project B. This spec is that sub-project. Mira (the salon owner) currently has no way to manage her own data. Products, services, and the schedule are only editable by directly running SQL or the seed script; bookings and inquiries (which land in RLS-protected, admin-only tables) are only visible via the Supabase dashboard's Table Editor. This spec gives her a real UI for all of that. ## Scope In scope: - Login screen for the admin account (already created in Phase 2A) - CRUD for products and services, including image upload to the `salon-images` Storage bucket (reserved but unused since Phase 2A) - Weekly schedule template editing, plus one-off schedule exceptions (holidays, special hours) - Viewing bookings and inquiries, with the ability to change their status (mark a booking cancelled/completed, mark an inquiry responded) Out of scope (explicitly deferred): - The public site reading live data instead of the mock data in `src/data/` — that's Sub-project C - Any reporting/analytics beyond the raw bookings/inquiries lists - Multi-admin support — the single fixed admin UUID model from Phase 2A is unchanged; this is a one-user admin panel ## Architecture Admin routes live under `/admin/*` in the existing `src/App.tsx` router, as a `React.lazy`-loaded subtree (mirroring the existing lazy-loaded `Home` page) so the public bundle's size is unaffected by admin code. A `ProtectedRoute` component wraps every admin route except `/admin/login`. It checks `supabase.auth.getSession()` on mount, subscribes to `onAuthStateChange()` for session expiry, and redirects to `/admin/login` when there's no session. RLS already gates every admin write on `auth.uid()` matching the fixed admin UUID (from Phase 2A), so no separate authorization check is needed client-side — an authenticated non-admin (there shouldn't be one; public signup is disabled) would simply get a `42501` from Postgres on any write attempt, the same protection every other write path in this backend already relies on. **No new frontend dependencies.** Data fetching and mutation go directly through `supabase-js` calls in `useState`/`useEffect`, matching the pattern already used throughout this codebase (e.g. `src/hooks/useBooking.ts`). This project has never used a data-fetching library (React Query, SWR, etc.), and nothing about this admin panel's scope — five sections, a single user, low request volume — needs the caching/sync machinery those would add. ## Data model changes No new tables. Two additions to what Phase 2A shipped: **1. `salon-images` Storage bucket policies.** The bucket exists (created manually in Phase 2A's Task 8) but is private with no policies. This spec adds: - The bucket flips from private to public (`update storage.buckets set public = true where id = 'salon-images'`), so `getPublicUrl()` works without signed URLs — product/service photos are marketing images, meant to be publicly visible on the site once Sub-project C wires the public pages to live data. - `select` on `storage.objects` for this bucket: public (`true`) - `insert`/`update`/`delete` on `storage.objects` for this bucket: gated to the admin UID, using the same pattern as every other admin-write RLS policy in this backend (`(select auth.uid()) = ''`) **2. Booking status transitions.** A separate, already-in-progress follow-up to Phase 2A is adding `after update of status` / `after delete` triggers on `bookings` so that cancelling a booking frees its slot in the `booked_slots` mirror table. This admin panel is the UI that calls `update bookings set status = 'cancelled' where id = ...` — no schema change needed here beyond consuming what that trigger already does once it lands. ## Components & pages | Component | Purpose | |---|---| | `AdminLogin` | Email/password form via `supabase.auth.signInWithPassword()` | | `ProtectedRoute` | Session check + redirect, wraps all routes below | | `AdminLayout` | Sidebar nav + ``, sign-out action | | `AdminProducts` | Table + create/edit form, delete with confirmation, image upload | | `AdminServices` | Same shape as `AdminProducts`, service-specific fields (duration, price-from) | | `AdminSchedule` | Weekly grid (7 rows: open/close time or closed) for `schedule_template`; a form below for one-off `schedule_exceptions` (date + closed-or-custom-hours) | | `AdminBookings` | Table of bookings, most recent first, status change actions | | `AdminInquiries` | Table of inquiries, status change actions (mark responded) | Visual style matches the public site's typography and color tokens (`src/styles/tokens.css`) and uses `framer-motion` for the same transition feel as the rest of the site, but with a dashboard layout (sidebar + content area) rather than the public site's marketing-page layout. ## Error handling Every mutation (create/update/delete/upload) shows an inline error message on failure instead of failing silently — RLS denials (`42501`), network failures, and Storage upload errors all surface to Mira in plain language. Session expiry (an expired JWT) redirects to `/admin/login` automatically via the `onAuthStateChange` listener, rather than presenting a raw 401. ## Testing Vitest + React Testing Library for every page and for `ProtectedRoute`, mocking `supabase-js` — consistent with how this codebase already tests components that touch external state. No new pgTAP tests are needed for table RLS (already covered in Phase 2A); the one new pgTAP addition is for the `salon-images` Storage bucket policies described above, since those are new database-enforced behavior this spec introduces. ## Open questions / risks None outstanding — all scope questions were resolved during brainstorming (bookings/inquiries get status-changing actions, not just viewing; image upload is in scope now, not deferred; schedule editing covers both the weekly template and one-off exceptions; visual style matches the public site; no new frontend dependencies).