6.5 KiB
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-imagesStorage 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'), sogetPublicUrl()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. selectonstorage.objectsfor this bucket: public (true)insert/update/deleteonstorage.objectsfor this bucket: gated to the admin UID, using the same pattern as every other admin-write RLS policy in this backend ((select auth.uid()) = '<admin-uuid>')
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 + <Outlet />, 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).