sitecrate-admin
sitecrate-admin is the internal control panel at admin.sitecrate.ca. It reads
from and writes to the same Supabase projects table that sitecrate.ca’s
intake form populates, and calls studio-website’s send-email function for all
email — it has no email function of its own.
It is a React 18 SPA. src/App.jsx holds the view components; navigation uses a
view state variable, not a router.
Authentication & roles
Section titled “Authentication & roles”Auth: Supabase Auth (email + password). The session is managed by the
Supabase JS client. On sign-in, the role is fetched from the user_roles table
(linked by user_id). If active = false, the user is signed out immediately
with a “deactivated” message. The role is stored in AuthContext
(src/lib/auth.jsx) and available app-wide via useAuth().
Roles — features are fully hidden (not just disabled) for roles without access:
| Role | Access |
|---|---|
admin | Everything |
engineer | Technical — stage, preview URL, build brief, admin notes |
sales | Client-facing — client info editing, revenue, analytics, comms |
Module layout
Section titled “Module layout”| Module | Purpose |
|---|---|
src/lib/auth.jsx | AuthContext, AuthProvider, useAuth |
src/lib/constants.js | PRICES, STAGES, labels, endpoint URLs |
src/lib/utils.js | fmtDate, fmtCurrency, daysSince, isNew, isStuck, calcRevenue, deriveSlug |
src/components/UsersPanel.jsx | Admin-only team management |
src/components/ActivityPanel.jsx | Combined project activity + email log |
view | Component | Purpose |
|---|---|---|
home | SitesHome | Revenue bar + grid of client cards + New project button |
sitecrate | SitecrateShell | GA4 analytics for sitecrate.ca (7d / 30d) |
client | ClientSiteShell | Project info strip, quick actions, pipeline kanban, email history |
Modals
Section titled “Modals”NewProjectModal — + New project in the SitesHome topbar. Creates
projects without the intake form (for clients from calls/emails). Requires
business name, client name, email; tier/billing/industry/domain/timeline
optional. Generates status_token via crypto.randomUUID(), inserts to
Supabase, and navigates to ClientSiteShell. Wrapped in try/finally so saving
always resets.
DetailPanel — overlay opened from a kanban card. Stage selector
(new → brief → building → review → live), slug input → Set .sitecrate.ca
(writes https://{slug}.sitecrate.ca to preview_url), freeform preview URL
(for live or non-sitecrate.ca URLs), client status-link copy, read-only brief,
client feedback, admin notes, save. Shows an amber warning after save if the
stage moved to review but no preview URL was set (the email was not sent).
BuildBriefModal — ⚡ Build brief in the ClientSiteShell action bar.
Derives the slug via deriveSlug(preview_url, business_name) (strips
.sitecrate.ca, falls back to a slugified business name, final fallback
'client'). Three copyable sections: preview URL, git clone command, and a
Claude Code prompt pre-filled with all intake data. Tier-aware — Tier 1 prompt
for presence, Tier 2 for growth.
Stage-transition emails
Section titled “Stage-transition emails”Auto-sent on DetailPanel save. Both call
https://sitecrate.ca/.netlify/functions/send-email:
| Transition | Condition | Email type |
|---|---|---|
→ review | preview URL must be set | review to client |
→ live | (none) | launch to client |
The review type returns HTTP 400 if preview_url is missing (server-side
guard).
Revenue calculation
Section titled “Revenue calculation”calcRevenue(projects) returns { mrr, oneTime, pipeline, customPipeline }:
- mrr — live projects with monthly billing
- oneTime — live projects with one-time billing
- pipeline — non-live
presence/growthprojects, valued at face price - customPipeline — count of non-live
platformprojects (custom-quoted, excluded from pipeline$to avoid misleading$0entries)
Pricing constants
Section titled “Pricing constants”PRICES = { presence: { 'one-time': 997, monthly: 49 }, growth: { 'one-time': 3997, monthly: 149 }, platform: { 'one-time': 0, monthly: 0 }, // Legacy — kept so old projects show correct values starter: { 'one-time': 1900, monthly: 89 }, standard: { 'one-time': 3400, monthly: 149 },}STAGE_WARN_DAYS = { new: 1, brief: 3, building: 7, review: 5, live: 999 }isStuck(p) → daysSince(p.stage_changed_at) > STAGE_WARN_DAYS[p.stage].
Real-time
Section titled “Real-time”The Supabase channel projects-changes subscribes to postgres_changes on the
projects table using import.meta.env.VITE_SUPABASE_SCHEMA || 'public', so
staging real-time updates work correctly against the staging schema.
Netlify functions (this repo)
Section titled “Netlify functions (this repo)”All three functions require a Bearer JWT and verify the caller against
user_roles (an active row is required) before doing anything. Clients pass
Authorization: Bearer ${session.access_token}.
| Function | Endpoint | Auth | Output |
|---|---|---|---|
ga-report.js | POST /.netlify/functions/ga-report | admin or sales | { summary, pages, sources, events } |
email-log.js | POST /.netlify/functions/email-log | any active member | Last 20 Resend emails matching subject |
admin-users.js | POST /.netlify/functions/admin-users | admin only | User CRUD (list/create/update) |
The studio send-email function is called with { type, token } (review/launch
on stage change); it resolves the project from the DB by token server-side.
Commands
Section titled “Commands”npm run dev # local dev server (port 5174 — main site uses 5173)npm run build # production build → dist/npm run preview # serve dist/ locallynpm run lint # ESLint — must pass before merging any PR