Files
braidsbymira/docs/superpowers/specs/2026-08-12-phase2b-admin-panel-design.md

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-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()) = '<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).