- KPI strip uses new <KPIStat> component (4 cards)
- New 'What needs attention' feed replaces 4 tabs of cards:
* no orders today → suggest campaign
* pending stops → link to stops
* usage > 85% → warn
* starter plan → upgrade prompt
- Quick actions + plan usage consolidated into one card
- Recent orders uses new <EmptyState>
- All 15 sections in a single grid (not 4 tabs), with 'All / Operations / Fulfillment / Management / Tools' filter
- 'Press ⌘K to search' hint under section grid
- All inline SVGs replaced with lucide-react
- All hardcoded colors replaced with new design tokens
- mount <CommandPalette /> in admin layout (Cmd+K)
- fetch getEnabledAddons(activeBrandId) for sidebar gating
- pass enabledAddons to AdminSidebar so Water Log / Route Trace hide when off
Phase 2 pattern extraction from the design/ui-revamp-2026-06 spec.
- KPIStat: extract the stat card pattern from DashboardClient (used four
times inline). Supports tone (default/primary/accent/warning/danger)
and an optional trend indicator. Reuses .admin-stat-card* CSS classes.
- EmptyState: generic centered icon + title + description + single
primary CTA. Action renders as <Link> when href is provided, else a
<button>; both use .ha-btn-primary.
- LoadingState: skeleton list driven by .ha-skeleton. Renders an
optional ha-eyebrow label above N rows (icon tile + label + value
placeholders). Each row uses the new admin design tokens.
- AdminBadge: tighten variants to the new design tokens. Legacy
variant= prop kept for back-compat (default/success/warning/danger/
info); new tone= prop adds neutral/primary/accent/success. All
colors now reference --admin-* tokens; no hardcoded hex values.
Added a 1px tone-aware border for stronger definition on the cream
canvas. AdminStatusBadge + AdminCountBadge updated to use the new
tone prop directly.
- design-system/index.tsx: re-export KPIStat, EmptyState, LoadingState
from the design-system barrel alongside AdminBadge.
npx tsc --noEmit clean.
Adds a self-contained <CommandPalette /> client component plus a
static data file that lists every admin page (per the new IA in
docs/superpowers/specs/2026-06-17-admin-redesign.md §4) and a
small set of quick actions.
Behavior:
- Toggles on Cmd+K (mac) / Ctrl+K (win/linux) at document level.
- Escape or backdrop click closes; Enter or click navigates.
- Fuzzy case-insensitive match on label / category / keywords,
top 8 results, highlighted match substring.
- ↑/↓ keyboard nav, mouse hover updates selection, auto-focus
on input when opened, body scroll lock while open.
- Visual: uses existing --admin-card-bg / --admin-border /
--admin-primary-soft / --admin-radius-lg / --admin-shadow-lg
tokens; fade-in 180ms, scale 0.98→1 animation.
- Returns null when closed (no DOM noise).
Out of scope for this pass:
- Component is NOT mounted in the admin layout (main thread wires
it in). The data file is the source of truth; the palette does
not import from AdminSidebar.
- localStorage-based Recent items are left as a TODO comment
in the component; v1 ships without persistence.
No new dependencies; uses lucide-react (already in package.json).
npx tsc --noEmit is clean.
- --admin-bg: warm cream (#FAF7F0)
- --admin-text-primary: ink black (#1A1814)
- --admin-text-muted: warm grey (#8A867E) — was too dim
- --admin-border: soft beige (#E8E4D7) — was too dirty
- --admin-primary: deep botanical (#1F4D2A)
- --admin-accent: warm amber (#B8761E) — the 'one thing to do'
- --admin-warning: amber (was #aba278 — looked like dirt)
- --admin-danger: warmer rust (#A8321C)
- sidebar: deeper, more refined (#2A2520)
- drop purple/blue/citrus stat icons in DashboardClient; use semantic tokens
- use --admin-warning instead of hardcoded #f59e0b in usage bar
- Remove Google Fonts @import from 4 components (SiteHeader, LandingPageWrapper,
TestimonialsAndCTA, FeaturesAndStats) — eliminate render-blocking external
request and font flash
- Replace all unloaded Cormorant Garamond / Playfair Display / DM Sans /
Plus Jakarta Sans references with the existing next/font variables
(Fraunces, Manrope, Fragment_Mono) — visual coherence with the design system
- Update 9 pages (blog, brands, changelog, maintenance, protected-example,
roadmap, security, waitlist, WaitlistForm) to use the loaded Fraunces
- Fix dead --font-geist / --font-jetbrains-mono references in
admin-design-system.css (now point to --font-manrope / --font-fragment-mono
which are actually loaded)
- Add atelier-pill, atelier-numerals, atelier-fineprint, atelier-canvas-soft
utility classes; .atelier-input:focus-visible refined ring
- Add .ha-field-textarea, .ha-scroll, .ha-skeleton to admin design system
- Extend prefers-reduced-motion guard to atelier-enter/stagger/shimmer
- Apply atelier-fineprint to login/not-found/error pages for consistency
- No structural changes; build passes, 0 type errors
User pain point: skeleton loading.tsx files made the app feel like
a sequence of page reloads, exposing backend latency. Replaced with
a single 1px shimmer bar + crossfade via React's <ViewTransition>.
Changes:
- Enable experimental.viewTransition in next.config.ts
- Add SmoothViewTransition wrapper (ViewTransition name=page-content)
- Add LoadingFade component: thin animated bar instead of skeleton
- Add RouteAnnouncer for a11y (screen readers + focus reset)
- Add ::view-transition-old/new CSS for the crossfade (220ms, no
jarring slide, respects prefers-reduced-motion)
- Wrap admin/tuxedo/IRD layout children in SmoothViewTransition
(sidebar/header/footer stay mounted; only the body fades)
- Replace 19 skeleton loading.tsx files with the fade component
Result: navigation now feels like a single app, not a series of
preload-and-render events. The user never sees a 'skeleton of the
page they're about to load.'
- Use StopTableClient in server page component
- Add view mode toggle (table/card) matching products page
- Match stats cards styling to products page
- Add consistent filter bar layout with AdminViewModeTabs
- Add card view for stops with inline delete confirm
- Improve row actions with Edit button + dropdown menu
- Add 'Today' badge and past stop dimming in card view
- Add EditStopModal for unified add/edit stop workflow
- Enhance StopTableClient with sortable columns (date, city, location, status)
- Add stats bar showing total, active, upcoming, and draft counts
- Improve pagination with 'Showing X-Y of Z' indicator
- Add 'Today' badge for current-day stops
- Dim past stops visually
- Add Edit action to row menu with icons
- Reduce page size to 25 for better UX
- AdminSidebar now renders BrandSelector when brands are available
- Stops page uses getActiveBrandId() instead of adminUser.brand_id
directly, so platform admins see all stops and brand selection
via the sidebar picker works correctly
- BrandSelector already existed and was wired up in the layout,
just never rendered in the sidebar
The page was importing from @/lib/supabase which is a mock client that
returns empty results. Replaced with pool.query() against the real
Postgres stops table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fetch dashboard stats server-side in admin/page.tsx to eliminate
client-side useEffect waterfall and the '—' placeholder flash
- Pass pre-fetched stats as props to DashboardClient
- Lazy-load UpgradePlanModal via next/dynamic (only loads when needed)
- Redesign stats cards with compact layout, smaller icons, Fraunces serif values
- Improve Quick Actions + Usage row: side-by-side on desktop, stacked mobile
- Clean up Recent Orders as a proper list with icon/name/time/amount/badge
- Add staggered fade-up entrance animations (0/60/120/180/240ms delays)
- Consolidate loading.tsx skeleton to match actual dashboard structure
- Mobile-responsive: 2-col stats on mobile, stacked usage, collapsible header
- LandingPageWrapper: Use dynamic year for copyright
- HeroSection: Add float-slow/float-slow-delayed keyframes, add prefers-reduced-motion media query
- TuxedoVideoHero: Add reduced-motion JS guard and CSS media query for accessibility
- getAdminUser now properly supports platform_admin with 0 brand links and loads full brand_ids
- createAdminUser action now inserts into admin_user_brands join table
- Admin layout surfaces the signed-in email on Access Denied
- AdminAccessDenied links to /login instead of dead-end /admin
- Main dashboard uses direct pool query instead of dead supabase shim
- Improved provision-admin.ts script for prod bootstrap (loads .env.production too)
The membership query in getAdminUser() was selecting role from
adminUserBrands.adminUserId, which is wrong - admin_user_brands has no
role column. Role is stored in admin_users. Also added try/catch
around withPlatformAdmin to prevent DB errors from throwing.
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject,
deleteObject, presigned URL helpers, and BUCKETS constant
- Wire product images, brand logos, and water log photos to MinIO via the
new storage client
- Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call)
- Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge
Function proxy)
- Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass
brand_settings.logo_url from all call sites
- Update email templates to use dynamic logoUrl instead of hardcoded Supabase
bucket URLs
- Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage,
TimeTrackingFieldClient; use brand_settings props + local public/ fallback
- Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to
public/ for local development
- Add MinIO env vars to .env.example (endpoint, access key, secret, buckets)
- Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props
- Fix admin/users.ts logoUrl type (null → undefined for optional string)
- Remove stale sb- cookie from wholesale-auth
- Migrate tuxedo/about page to remove supabase import and use pool query for
wholesale_settings lookup
The products list page at /admin/products still queried the legacy Supabase
mock client, which returns an empty array. Combined with the SaaS schema
renaming brands → tenants, the platform admin's brand picker for new
products was silently failing (getBrands SELECTed from a non-existent
'brands' table), making 'Add Product to catalog' look like a no-op.
- src/app/admin/products/page.tsx: replace supabase.from('products') with
a Drizzle query using withPlatformAdmin (cross-tenant) or
withTenant(brandId, ...) (scoped). Map new columns back to the legacy
Product shape the existing UI consumes:
* price_cents (integer) → price (dollars)
* tenant_id → brand_id
* product_images LEFT JOIN for first image per product
* default type='pickup', is_taxable=false (columns not in SaaS schema)
- src/actions/admin/users.ts (getBrands): query tenants table instead of
the legacy brands table.
Verified locally against the seeded dev DB: 8 products across 2 tenants
now render in the catalog, and the brand picker populates with both
tenants for platform admins.
Previously, any non-empty AUTH_GOOGLE_ID + AUTH_GOOGLE_SECRET would
trigger the Google button. That broke dev/CI setups where the env
vars are placeholder strings (e.g. 'dummy-google-client-id') — the
button would render and immediately 401 from Google with 'invalid_client'.
Real Google OAuth client IDs always end in '.apps.googleusercontent.com'.
Gate hasGoogle on that suffix so the login page falls back to the
credentials form (or the 'not configured' message) when the values
aren't real.
- analytics.ts: rewrite getReportsSummary, getRevenueChart, getSalesByProduct,
getContactGrowth, getRecentOrders, getConversionFunnel against pool + new
orders/customers schema. Drops retired columns (subtotal, pickup_complete)
and re-implements the SQL by hand.
- import-orders.ts: bulk import via withTx using orders + orderItems + customers
Drizzle tables, computes total_cents from current product prices.
- import-products.ts: rewrite to use withTenant(brandId) and Drizzle products
table.
- products/create-product.ts, update-product.ts, upload-image.ts: switch to
withTenant + Drizzle; image_url moves to product_images table.
- reports.ts: rewrite against pool + new orders schema.
- route-trace/lots.ts: stub functions (route-trace feature retired from SaaS
rebuild — harvest_lots table not in db/schema). Uses discriminated union
return types so consumer narrowing works in both branches.
- settings/features.ts: switch to withTenant + Drizzle brandSettings.
- shipping.ts: switch to pool + Drizzle orders/orderItems.
- api/v1/referrals/route.ts: fix typecheck (referred_user_id undefined → 'anonymous').
Typecheck: clean. Tests: 22/22 pass. Build: succeeds.
Next.js 16 enforces 'one of middleware.ts OR proxy.ts' - both can't
coexist. Our src/middleware.ts (Auth.js v5) is the canonical one;
src/proxy.ts was the legacy dev_session auto-issuer.
- db/seed.ts: upserts admin@route-commerce.local with a scrypt password
hash (default password 'admin', override with SEED_ADMIN_PASSWORD)
and grants them the platform_admin role on the Tuxedo tenant so
getAdminUser() resolves it for the Credentials provider's authorize
function
- src/app/login/page.tsx: reads ?error=... from the URL and passes
hasCredentials + seededEmail + error to the client so the form
pre-fills in dev and surfaces 'Invalid email or password' cleanly
- src/app/login/LoginClient.tsx: adds the email + password form below
the Google button, with a divider, dev-mode pre-fill, and local error
handling for client-side failures (server-side failures come back
through the ?error=... param)
- src/auth.config.ts: edge-safe Auth.js config (no pg, no Credentials)
imported by the middleware; exports authConfig + isDevLoginEnabled
- src/lib/auth.ts: extends authConfig with the Credentials provider
(Node-only) for email + password sign-in via users.password_hash
- src/middleware.ts: uses authConfig (edge-safe) instead of the full
src/lib/auth.ts so the edge bundle stays small
- src/actions/auth-actions.ts: adds signInWithCredentials that calls
the Credentials provider; signInWithGoogle stays the production path
- Migration 0002 adds nullable password_hash to users (idempotent)
- src/lib/passwords.ts: encode/verify with self-describing format
(algo$N$salt$hash) so we can migrate to a stronger KDF later
- Schema adds passwordHash column; OAuth-only users leave it null
- Create src/lib/db.ts (shared pg.Pool, query/withTx helpers, server-only)
- Add @types/pg devDep
- Migration 204: add email, auth_provider, auth_subject columns to
admin_users; backfill from auth.users; new upsert_admin_user accepts
multi-provider args; new get_admin_user_for_session RPC resolves
Auth.js session id (UUID or Google sub) to an admin row
- Refactor getAdminUser() to use pg + new RPC; auto-provisions on first
Google sign-in using session.user.email
- Refactor updatePasswordAction to call update_user_password via pg
(drops the Supabase REST hop)
- Delete orphaned src/actions/login.ts (replaced by auth-actions.ts)
- Drop remaining DEV_FORCE_UID references in users.ts; dev path now
uses dev_session cookie (the only cookie the dev flow can set)
- Update AdminUser type: user_id is now string | null (Google users
have no Supabase auth id); add email + auth_provider fields
- Fix downstream type errors: StopProductAssignment.callerUid accepts
null; pickup.ts performedBy widens to string | null
- Bump vitest config, tests/, and other earlier cleanup changes
The /admin page was throwing a Server Components render error (digest
4266906817) in production. The exact throw point was not visible from
the browser (Next.js hides it for security) and server logs were not
available at debugging time.
The layout and page both call data-loading functions (getActiveBrandId,
listBrandsForAdmin, supabase brands lookup) that could throw on a
transient DB/network failure, and these calls had no try/catch. A
single failed call would crash the entire admin shell.
Wrap each call in try/catch with console.error so:
1. The page renders with sensible defaults if a call fails
2. The actual error is logged server-side (visible via the digest in
the admin error boundary or PM2/Docker logs)
3. The admin shell stays functional even if a single data source is
down
No behavior change on the happy path.