# Phase 9 — Super Administrator, Media Manager, Business Branding, Loyalty Redesign, Home Screen Redesign and Google Reviews: Summary A large post-Phase-8 pass adding a platform-level administration account, a single reusable Media Manager that becomes the standard image picker everywhere, full post-install editability of every branding asset, a restyled and properly-configured loyalty card, a real Google Reviews pipeline, and a sticky-tabbed Home Screen redesign (Services/Portfolio/Reviews/Details) built on top of a new owner-managed gallery. Executed in nine dependency-ordered sub-phases (9.0–9.9, tracked as such in-session), each fully typechecked, linted, and integration-tested against a real MySQL database before moving to the next — no phase was left partially wired. --- ## 1. Super Administrator A platform-level account with every Owner permission, provisioned from environment variables (`SUPER_ADMIN_EMAIL`/`PASSWORD`/`FIRST_NAME`/`LAST_NAME`) rather than the installation wizard — deliberately **not** a hidden staff member. It has no `StaffProfile` row at all, which is the whole design: every exclusion the brief asked for (not in the Team list, not selectable as a barber, never receives an appointment, not counted in staff totals, not in customer-facing staff lists) falls out **structurally** from that absence — `StaffProfile`-scoped queries simply never see it — rather than needing a filter added at each of the ~6 call sites that would otherwise need one. `hasPermission()` (`src/lib/auth/permissions.ts`) short-circuits `true` for any permission when `User.isSuperAdmin` is set — safe because `OWNER_PERMISSIONS` already **is** the full `Permission` enum, so "all Owner permissions" and "unconditionally true" are the same thing. `scripts/ensure-super-admin.ts` is idempotent and re-hashes the password every run (wired into `docker/entrypoint.sh`, after migrations), so rotating `SUPER_ADMIN_PASSWORD` and restarting the container is how you change it; it refuses to silently take over an existing non-super-admin account with the same email. **Real architectural cost, paid honestly rather than papered over**: every owner page that resolved its business via `getActingStaff()` (a `StaffProfile` lookup) would otherwise 403 a Super Administrator with no such profile. Rather than special-case each one, a new `getActingBusinessContext()` helper (`src/lib/business/get-acting-business-context.ts`) resolves `{userId, businessId, businessName, businessTimezone}` for **either** a real staff member **or** a Super Administrator (falling back to the app's one business, since v1 is single-business) — and all 16 owner pages/actions that only ever needed `businessId`/`timezone` from `getActingStaff()`, not real staff identity, were retrofitted onto it (verified per-file that none of them used `staff.id` for anything beyond the redirect gate). Pages doing genuine staff-ledger work (loyalty stamp authorship, appointment reassignment) still require real `StaffProfile` identity and are out of scope for this account by design — "purely for administration," per the brief. --- ## 2. Media Manager No upload mechanism existed anywhere before this phase — every image field was a plain-text URL `Input`. Now there is exactly one: `MediaManagerModal` (Upload / Library / External URL tabs) plus the field-level `MediaPicker` wrapper (preview + Change + Remove), both in `src/features/media/components/`. It backs every image field migrated this phase: business logo, cover image, welcome backgrounds, favicon, PWA icon, loyalty card background, staff photo, service photo, and the new gallery/portfolio system. - **Storage**: local disk under `public/media//...`, served for free by Next's own static file handling — no custom GET route needed, only the upload path (`src/lib/media/media-storage.ts`). A deliberate v1 choice for this app's single-container Docker model, not a cloud integration; `docker-compose.local.yaml` gained a `next-chair-media` named volume so uploads survive a rebuild, and the Dockerfile pre-creates `public/media` owned by the non-root user before that volume first attaches. - **Schema**: new `MediaAsset` model (url, dimensions, size, mime type, category, uploader). - **Upload path**: a Server Action (`src/features/media/actions/media.ts`), not a REST route — Next.js Server Actions accept `FormData`/`File` natively, so this stays consistent with every other mutation in the app rather than introducing a one-off API endpoint. SVG is rejected (stored-XSS risk via inline `