Files
route-commerce/docs/qa/audit-2026-06-25/INVENTORY.md
T
Nora 0ac4beaaa8 fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call
- Convert getAdminUser() → requireAuth() across 73 admin action files
- Add getSession() to public/wholesale server actions
- Fix multi-line return type corruption from earlier auto-fixers
- Move FedEx token cache to non-'use server' module
- Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD,
  EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS
- Update Stripe API version 2026-05-27 → 2026-06-24
- Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient
- Fix 51 TypeScript errors (return type corruption, missing imports)
2026-06-25 23:49:37 -06:00

28 KiB
Raw Blame History

QA Audit Inventory — 2026-06-25

Generated for Loop 010 — full product evaluation. Scope: Admin shell (all 19 modules) + public/auth pages, tested as platform_admin via dev_session cookie. Source: 5 parallel explore subagents enumerated every UI control, server action, state, and workflow.

Inventory organization

  • R1R4: Role/identity assumptions
  • A1A11: Admin shell, layout, dashboard
  • M1M11: Admin modules (orders, stops, products, pickup, shipping, communications, wholesale, settings, analytics, users, import)
  • P1P19: Public + auth pages
  • T1T6: Cross-cutting patterns, design system, server-action catalog

Each item: route → purpose → buttons → forms → modals → states → workflows → server actions.


R — Roles & identity

File: src/lib/admin-permissions.ts lines 36-39. Gated by NODE_ENV !== "production". Values: platform_admin, brand_admin, store_employee. Returns admin user via buildDevAdmin(role) bypassing Neon Auth.

R2 — Real Neon Auth (Better Auth)

File: src/lib/auth.ts. Email/password sign-in, password reset, OAuth via signInWithGoogleAction. Session via getSession(). Not exercised in this audit (auth is stubbed).

R3 — RBAC permission flags

File: admin_users table; flags: can_manage_orders, can_manage_products, can_manage_stops, can_manage_customers, can_manage_wholesale, can_manage_billing, can_manage_settings, can_manage_water_log, can_manage_time_tracking, can_manage_route_trace, can_manage_reports, can_manage_communications, can_manage_pickup, can_manage_messages, can_manage_refunds, can_manage_users, active. Default brand_admin = orders/products/stops/customers/pickup/messages/reports = true, rest false.

R4 — Tenant scoping (brand isolation)

  • App-layer: effectiveBrandId = brandId ?? adminUser.brand_id ?? null; threaded through page → client.
  • DB: current_brand_id() GUC + RLS policies on most tables (brand_id = current_brand_id() OR is_platform_admin()).
  • Platform admin: brand_id IS NULL, sees all brands via withPlatformAdmin().

A — Admin shell, layout, dashboard

A1 — /admin/layout.tsx

Server root layout. Gates access via getAdminUser(), redirects on must_change_password. Mounts: AdminSidebar (desktop), MobileTabBar (mobile via v2 layout), CommandPalette, PWAInstallPrompt, ToastContainer, RouteAnnouncer, SmoothViewTransition. Brand/brand-list/addons fetched with empty fallbacks on error.

A2 — /admin/page.tsx (legacy dashboard)

Redirects to /admin/v2. Two paths: role === "store_employee" → 2-card landing (Pickup Lookup + Wholesale); otherwise fetches getDashboardStats() + getBillingOverview() server-side, renders DashboardClient.

A3 — /admin/v2/layout.tsx + /admin/v2/page.tsx (mobile-first dashboard)

Mobile "Today" dashboard. Fetches summary/stops/orders in parallel. Promise.all failure → silent empty state. Renders EmptyState ("No brand selected" / "Nothing to show yet"). pending_fulfillment > 0 → amber callout link /admin/v2/orders?status=placed. Top 5 today stops + top 5 pending orders. Pull-to-refresh wrapper.

A4 — AdminShell.tsx

Responsive shell: useMediaQuery("(min-width: 1024px)") swaps desktop sidebar vs mobile (OfflineBanner + MobileTabBar). Mounts OfflineBanner only on mobile. pb-20 mobile bottom padding for tab bar.

A5 — AdminSidebar.tsx

Desktop nav + mobile slide-in. Nav groups: Workspace / Operations / Communications / Growth / Tracking / Insights / Settings. Brand selector pill. Sign-out button (signOutAction). Hamburger on <lg. Focus trap + arrow-key navigation + body scroll lock + ESC closes. requiresPlatformAdmin filter; enabledAddons hides Water Log/Route Trace when disabled.

A6 — MobileTabBar.tsx

Bottom nav for /admin/v2/*. 5 tabs: Home / Orders / Stops / Products / More (opens MoreSheet). Active highlight via pathname match.

A7 — MoreSheet.tsx

Right-anchored native <dialog>. Sections: Operations (Pickup, Shipping, Reports, Analytics), Marketing (Communications, Sales), Settings (Settings, Advanced). Saves focus on open, restores on close.

A8 — CommandPalette.tsx

Cmd/Ctrl+K modal. Fuzzy search over static entry list (./command-palette-data.ts). Up to 8 results. ↑↓ to navigate, Enter to select, ESC closes. body.style.overflow = "hidden" while open. Honors prefers-reduced-motion.

A9 — BrandSelector.tsx

Pill dropdown for active brand. Calls setActiveBrand(brandId) server action, then router.refresh(). Outside-click + ESC close. "All brands" option for platform_admin only. Multi-brand badge.

A10 — AdminAccessDenied.tsx

Static card with "Go to Login" + "Return to homepage" links.

A11 — OfflineBanner.tsx

Sticky top banner: warning-soft when syncing, danger-soft when offline. Polls pendingCount every 1 s. Hidden pre-mount to avoid hydration mismatch.


M — Admin modules

M1 — Orders

  • /admin/orders (legacy) → AdminOrdersPanel: KPI tiles, status tabs (all/pending/picked_up), search (name/phone/id), stop multi-filter dropdown, table with checkbox + bulk pickup, pagination (PAGE_SIZE 20), + New Order modal. Modal fields: customer name (required), email (optional), phone (optional), stop (optional), items[product+qty+price+fulfillment]. Workflows: create→full-page-reload, single pickup (optimistic), bulk pickup (loop).
  • /admin/orders/[id]OrderEditForm + OrderPaymentSection + OrderPickupAction. Edit fields: items qty/price, discount (must be ≥ 0), customer name (required), email/phone, status (pending/confirmed/cancelled — fulfilled not exposed), pickup toggle, internal notes. Payment fields: processor (none/manual/stripe/square/cash/venmo/other), payment status, transaction id, refund amount (0 < x ≤ remaining balance) + reason.
  • /admin/orders/new → redirect to /admin/orders?new=true (opens modal).
  • /admin/v2/orders → card-style list, ?status= filter (placed/ready/picked-up/cancelled), LIMIT 50, no pagination.
  • /admin/v2/orders/[id] → mobile detail with FulfillmentTimeline + sticky action bar (OrderActionButtons).

M2 — Stops

  • /admin/stopsAdminStopsPanel: status tabs (all/active/draft/inactive), search city/location, Add Stop modal, Upload Schedule modal, per-row Edit/Publish/Duplicate/Delete. Delete uses inline popover confirm. PAGE_SIZE 50.
  • /admin/stops/[id] → product assignment + edit form + MessageCustomersSection.
  • /admin/stops/new → inline NewStopForm (not modal). Fields: city/state/location/date/time (required), brand select, active, address/zip/cutoff (optional). Also supports ?duplicate={id}.
  • /admin/v2/stops → card list bucketed by Morning/Afternoon/Evening/Anytime, ?date= filter, LIMIT 200.
  • Modals: AddStopModal, EditStopModal, AddLocationModal, EditLocationModal, LocationsTab.

M3 — Products

  • /admin/productsProductsClient (legacy): list with brand scoping (platform_admin sees all). No client pagination.
  • /admin/products/newNewProductForm: name (required), description, price, type (pickup/shipping/both), brand (locked for non-platform_admin), taxable, pickup_type, active, image upload (drag/drop, 5MB cap, client-side resize to 1200px).
  • /admin/products/import → CSV upload → parseProductCSV → preview → importProductsBatch.
  • /admin/v2/products → mobile card list with ?filter= (all/in-stock/low/out/hidden) + StockAdjustButton per card.

M4 — Pickup

  • /admin/pickupDriverPickupPanel. Pending orders (filter by stop + search), per-order Pick Up button (calls markPickupComplete). Picked-up section collapsible (last 72h). Toast ✓ Order picked up 3s.

M5 — Shipping

  • /admin/shippingShippingFulfillmentPanel. Pre-loaded via getShippingOrders(). (Details in deeper inspection.)
  • /admin/settings/shipping (advanced subtab) → AdvancedShipping: static UI mock — no server action calls. Carrier select (fedex/ups/usps/dhl), account/api fields, eye toggle, Test Connection is simulated (1s setTimeout), Save Settings is simulated (500ms setTimeout).

M6 — Communications (Harvest Reach)

Unified hub at /admin/communications with 8 tabs:

  • Campaigns: list with type/status filters. New Campaign modal (name + campaignType). Edit panel: name, type, template, audience target (stop/zip_code/customer_history/all_customers), stop_id/dates, subject, body, schedule (now/later + datetime-local). Save Draft / Schedule Campaign / Send Campaign buttons. upsertCampaign, deleteCampaign, getCampaignTemplates, getCommunicationSegments, previewCampaignAudience, sendCampaign server actions.
  • Compose: legacy redirect target.
  • Templates: list + edit (template_type, subject, body_text, body_html, campaign_type). Server: getCommunicationTemplates, getTemplateById.
  • Contacts: search + source filter, pagination (50/page, offset), per-row delete (confirm), export CSV (exportContacts). Bulk: none. Card layout on <sm.
  • Segments: redirect → ?tab=segments.
  • Logs: MessageLogPanel — search by email/subject/status, status filter, pagination (20/page, numeric 1-5). Stats cards: total/delivered/failed/pending. getMessageLogs (server-side limit 100).
  • Analytics: redirect → ?tab=analytics.
  • Settings (/admin/communications/settings): CommunicationSettingsForm — sender email/name, reply-to, footerHtml (supports {unsubscribe_url} placeholder).
  • Abandoned Carts (/admin/communications/abandoned-carts): 3-step sequence (1h/24h/48h). Stats (total/active/recovered/expired), filter, pagination (25/page, 7×7 prev/next), View/Close/Resend actions per cart. Modal: cart detail with items + timestamps.
  • Welcome Sequence (/admin/communications/welcome-sequence): 4-email dashboard.

Server actions: getCommunicationCampaigns, getCommunicationTemplates, getCommunicationContacts, getMessageLogs, getCommunicationSettings, upsertCommunicationSettings, getAbandonedCarts, manuallyCloseAbandonedCart, resendAbandonedCartEmail, sendStopBlast, previewContactImport, importContactsBatch, uploadContactsToBucket, processBucketImport, listImportHistory, exportContacts.

M7 — Wholesale

  • /admin/wholesaleWholesaleClient (mounted by getActiveBrandId). Dashboard tabs: Customers / Orders / Pricing / Settings.
  • Customers: list with search, status filter (active/inactive/suspended/pending), pagination, bulk "Send Price Sheet" (counts).
  • Orders: list with status filter, bulk Fulfill / bulk Deposit (with confirms).
  • Pricing: per-customer price overrides.
  • Settings: WholesaleSettingsForm — require_approval, min_order_amount, online_payment_enabled, pickup_location, fob_location, from_email, invoice_business_name.

Server actions: getWholesaleCustomers, getWholesaleOrders, updateWholesaleCustomer, sendPriceSheetToCustomer, bulkFulfillOrders, recordWholesaleDeposit, getWholesaleSettings, updateWholesaleSettings.

M8 — Settings (/admin/settings)

Main hub. Tabs (URL anchor): brand / addons / users / billing.

  • Brand (/admin/settings #brand): BrandSettingsForm — company info, address, 4 logo uploads, email/invoice branding, storefront customization (tagline, hero image, about, footer), brand colors (4 pickers), tax settings (collect toggle + nexus states), feature toggles (wholesale/zip/schedule-pdf/text-alerts), schedule PDF footer.
  • Add-ons (/admin/settings #addons): BrandFeatureCards — grid of cards from ADDON_CATALOG. Enable/Disable toggle (with GlassModal confirm) + optimistic with rollback on failure.
  • Users (/admin/settings #users): list of admin users + CreateUserModal. Create fields: email (required, must include @), password (≥6), display name, phone, role (3-way radio gated by caller), brand (required when brand_admin/store_employee), 9 permission toggles. Surfaces temp password on success.
  • Billing: tier display + Stripe portal link (if customer_id set).
  • Payments (/admin/settings/payments): PaymentSettingsForm — provider radio (None/Stripe/Square), Stripe OAuth button (/api/stripe/oauth), Square OAuth (/api/stripe/oauth → Square). Square Location ID validated L prefix. Inventory Mode radio (None/RC→Square/Square→RC/Bidirectional). Sync Products/Orders/All Now buttons. Wholesale Webhook Log.
  • Square Sync (/admin/settings/square-sync): SquareSyncSettingsClient — provider/account/last sync/status grid + sync settings + manual sync + last 50 sync log entries.
  • AI (/admin/settings/ai): link cards only.
  • Advanced (/admin/advanced): aggregator cards linking to sub-pages.
    • Integrations (AdvancedIntegrations): Resend + Twilio credential cards (Test Connection + Save).
    • AI Tools (AdvancedAIPanel): provider cards (OpenAI/Anthropic/Google/xAI/Custom), API key (eye toggle), org ID (OpenAI only), custom endpoint (Custom only), model chips with cost display, Test + Save.
    • Square Sync (AdvancedSquareSync): static stub — App ID/Access Token/Location ID, sync toggles hardcoded ON, Test/Save purely client-side setTimeout.
    • Shipping (AdvancedShipping): static mock — same as M5.
    • Webhooks: "Coming Soon" placeholder.
    • Payments (AdvancedPayments): Stripe Connect onboarding — Connect with Stripe / Complete Setup / Stripe Dashboard / Disconnect. createStripeConnectLink, refreshStripeConnectLink, disconnectStripeConnect, createStripeDashboardLink, getStripeConnectStatus.

M9 — Analytics (/admin/analytics)

AnalyticsDashboard. KPI cards (revenue, orders, customers, AOV), revenue chart, top products, recent orders, customer growth ring, conversion funnel. Period selector (7D/30D/90D/1Y) — purely visual, no server refetch. Refresh + Export Report (no-op). Server: getAnalyticsMetrics, getRevenueChart, getTopProducts, getRecentOrders, getCustomerGrowth, getConversionFunnel.

M10 — Users (/admin/users)

Redirect → /admin/settings#users.

M11 — Other admin modules

  • Import Center (/admin/import): ImportCenterClient — 4-step wizard (Upload → Analyze → Preview → Import). Accepts CSV/XLSX/XLS/TXT (≤5,000 rows / 10 MB). AI-powered type detection (products/orders/contacts/stops). Per-column mapping dropdowns. Brand select (platform_admin only). Server: analyzeImport, executeImport.
  • Sales import (/admin/sales/import): CSV → parseOrderCSVimportOrdersBatch. Brand ID (UUID text input) required.
  • Time Tracking (/admin/time-tracking): TimeTrackingAdminPanel. Tabs: Summary / Workers / Tasks / Logs / Settings. Workers modal (name, role, language, active). Tasks modal (name, name_es, unit, active). Logs: date range + worker + task filters, pagination (50/page offset). Export CSV → /api/time-tracking/export. Tuxedo-only.
  • Water Log (/admin/water-log): WaterLogAdminPanel. Tabs inline: Headgates / Users / Entries. Add Headgate (name, unit, notes) inline. Add User (name, role=irrigator/water_admin, language=en/es, phone). 4-digit PIN auto-generated + shown in PinBanner. Per-headgate: Edit (link), Rotate token, Delete. Per-entry: date range + headgate + user + method filters. Export CSV (5,000 entries). Preview Report = window.alert of text. Settings: /admin/water-log/settings — admin portal toggle, session duration slider (1-168h), coarse permission flags (edit/delete/export), alert phone. Tuxedo + can_manage_water_log.
  • Headgates (/admin/water-log/headgates): HeadgatesManager. Add inline + Edit modal + QR modal (Preview/Print/Download tabs). Bulk QR print via POST /api/water-qr-sheet → new window. Per-headgate QR endpoints: /api/water-qr-label, /api/water-qr?token=....
  • Route Trace (/admin/route-trace, /admin/route-trace/lots, /admin/route-trace/lookup, /admin/route-trace/settings): all render the same RouteTracePage shell. Server: getRouteTraceStats, getRouteTraceLots, getHarvestLotsReadyToHaul, getFieldYieldSummary, getInventoryByCrop, getRecentLotEvents. Lot detail at /admin/route-trace/lots/[id] with timeline + order fulfillment. New lot → modal in Lots tab.
  • Launch Checklist (/admin/launch-checklist): 8 categories × 4 tasks each (32 tasks). Progress is purely cosmetic (hardcoded 20% / 7 of 32). Checkbox buttons have no handlers. Export PDF / Mark All Complete / Mark Launch Complete all no-ops.

P — Public + auth pages

P1 — /login

Email/password + Google OAuth + dev-mode shortcuts (Platform/Brand/Store buttons).

  • Form: email (required, autoComplete=username), password (required, autoComplete=current-password).
  • Buttons: Sign in (submit), Continue with Google (OAuth), 3 dev shortcuts.
  • Links: /forgot-password (Forgot?).
  • States: loading (sign-in/Google), error (inline role=alert), success → redirect /admin.
  • Dev shortcuts only when NODE_ENV !== "production".

P2 — /change-password

Force password update.

  • Form: new password (required, minLength 8), confirm password (required).
  • Buttons: Update Password (submit), "Sign out instead" link → /logout.
  • Server: POST /api/auth/change-password.

P3 — /reset-password

New password from reset link.

  • Form: new password + confirm (both required, ≥ 8 chars, must match).
  • Server: POST /api/auth/reset-password.

P4 — /logout

Effect-only: spinner + "Signing out..." → calls /api/auth/sign-out/login.

P5 — /maintenance

Static splash. No controls. mailto + /contact links.

P6 — / (homepage)

Marketing landing (LandingPageClientHeroSection). No forms. CTA links: /login, /brands, /contact. Scroll indicator.

P7 — /pricing

Pricing page. BillingToggle (Monthly/Annual, aria-pressed, "-25%" badge). 3 plan CTA buttons → /admin. Compare table expand/collapse. 8 FAQ accordion toggles. No form/select/textarea.

P8 — /contact

Contact form (simulated setTimeout submit).

  • Fields: name (required), email (required), topic select (General/Orders/Wholesale/Partner/Tech), message textarea (required, rows 5).
  • Buttons: Send Message (submit, spinner), Send another message (success state).
  • tel + mailto links.
  • States: idle, isSubmitting, success.

P9 — /blog

Static. Newsletter form (decorative — no submit handler). "Read More" links to /blog/getting-started-with-route-commerce (slug not implemented). 3 "Download →" buttons (no handlers).

P10 — /changelog

Static timeline. Links: /admin, /, RSS feed /api/feed/changelog.xml, /waitlist.

P11 — /roadmap

Static 3-column roadmap.

  • Suggestion form: feature title (text), description (textarea), category (select). No submit handler.
  • 6 Upvote buttons — no handlers.
  • Anchor jump #suggest.

P12 — /security

Static. mailto security@routecommerce.com for vulnerability reports.

P13 — /privacy-policy + /terms-and-conditions

Pure static legal text.

P14 — /brands

Partner brands showcase. Per-brand <Link href="/${slug}"> "Visit Store" + "View Stops". 3D-tilt parallax.

P15 — /waitlist

<WaitlistForm>.

  • Fields: name (optional text), email (required), referral select (Google/Social/Friend/Event/Podcast/Blog/Other).
  • Button: Join the Waitlist (spinner → success card).
  • Server: POST /api/waitlist.
  • States: idle, isSubmitting, error, success.

P16 — /cart

<CartClient>.

  • Per-item buttons: (decrease qty), + (increase qty), Remove.
  • Modal: "Choose Pickup Stop" (stop buttons + Cancel).
  • Buttons: Continue to Checkout (state-gated: Cart is Empty / Select Pickup Stop First / Remove Incompatible Items First / Choose Correct Stop / Continue Shopping).
  • States: empty, stop mismatch alert, incompatible items, availability error, loading stops.

P17 — /checkout

<CheckoutClient> + Stripe Express iframe.

  • Fields: customer_name (text), customer_email (required), customer_phone (tel optional), stop_id (select required when no stop), shipping_address/city/state (maxLength 2 upper)/postal_code (when ship items).
  • Buttons: Change pickup stop, "Use secure hosted checkout →" (POST createRetailStripeCheckoutSession), <Link href="/"> Back to storefront.
  • States: empty cart, hostedLoading, hostedError, hasPickup/hasShed/hasStop/hasShip branching.

P18 — Auth API routes

Route Method Behavior
/api/auth/sign-in POST {success} or 400/401/500 with {error,message}.
/api/auth/sign-out POST Server sign-out + redirect /login.
/api/auth/forgot-password POST Always {success:true} (anti-enumeration). Calls requestPasswordReset.
/api/auth/reset-password POST Min 8 chars.
/api/auth/change-password POST Auth-required; uses setUserPassword.
/api/auth/[...nextauth] GET/POST Neon Auth handler proxy.

P19 — Other public pages

  • /water — likely the field portal; brief enumeration.
  • /test-simple.html — diagnostic HTML.
  • /api/feed/changelog.xml — referenced but not in scope.

T — Cross-cutting patterns

T1 — Server-action catalog (top-level actions touched by the audited routes)

getAdminUser, getAdminOrders, getAdminOrderDetail, markPickupComplete, createAdminOrder, updateOrder, updateOrderItem, deleteOrderItem, createRefund, getShippingOrders, createStop, updateStop, deleteStop, publishStop, assignProductToStop, unassignProductFromStop, createLocation, updateLocation, deleteLocation, createProduct, uploadProductImage, importProductsBatch, parseProductCSV, analyzeImport, executeImport, getAnalyticsMetrics, getRevenueChart, getTopProducts, getRecentOrders, getCustomerGrowth, getConversionFunnel, getCommunicationCampaigns, getCommunicationTemplates, getCommunicationContacts, getMessageLogs, getCommunicationSettings, upsertCommunicationSettings, getAbandonedCarts, manuallyCloseAbandonedCart, resendAbandonedCartEmail, sendStopBlast, previewContactImport, importContactsBatch, exportContacts, getWholesaleCustomers, getWholesaleOrders, getWholesaleSettings, updateWholesaleSettings, sendPriceSheetToCustomer, bulkFulfillOrders, recordWholesaleDeposit, createAdminUser, getAdminUsers, getBrands, getPaymentSettings, savePaymentSettings, syncSquareNow, getSyncLog, getResendCredentials, saveResendCredentials, testResendConnection, getTwilioCredentials, saveTwilioCredentials, testTwilioConnection, getStripeConnectStatus, createStripeConnectLink, refreshStripeConnectLink, disconnectStripeConnect, createStripeDashboardLink, getWaterIrrigators, getWaterHeadgatesAdmin, getWaterEntries, createWaterHeadgate, updateWaterHeadgate, deleteWaterHeadgate, regenerateHeadgateToken, createWaterUser, resetWaterIrrigatorPin, deleteWaterUser, getWaterAdminSettings, saveWaterAdminSettings, regenerateAdminPin, getTimeTrackingWorkers, getTimeTrackingTasks, getTimeTrackingSummary, getWorkerTimeLogs, createTimeWorker, updateTimeWorker, deleteTimeWorker, resetTimeWorkerPin, createTimeTask, updateTimeTask, deleteTimeTask, getRouteTraceStats, getRouteTraceLots, getRouteTraceLotDetail, getLotOrders, getHarvestLotsReadyToHaul, getFieldYieldSummary, getInventoryByCrop, getRecentLotEvents, signOutAction, signInWithGoogleAction, toggleBrandFeature.

T2 — Server-action categories

  • CRUD: orders, stops, products, customers, campaigns, templates, contacts, wholesale_, time_tracking_, water_log_*, harvest_lots.
  • Bulk: bulkFulfillOrders, sendPriceSheetToCustomer (wholesale); markPickupComplete loop (orders).
  • Auth: sign-in, sign-out, password reset/change, OAuth.
  • Integration side-effects: syncSquareNow, sendCampaign, sendStopBlast, savePaymentSettings → Stripe OAuth URL generation.
  • Reads: get* actions returning typed rows for page/server-component hydration.

T3 — Server-component vs client-component split

  • Every route's page.tsx is "use server" and resolves getAdminUser() + brand + initial datasets in parallel.
  • Heavy interactive panels (orders, stops, products, communications, settings, water-log) are "use client" components mounted by their page.
  • v2 dashboard surfaces (/admin/v2/*) lean more on server components with pool.query directly.

T4 — Auth gating patterns

  • Page-level: if (!adminUser) redirect("/login") or <AdminAccessDenied />.
  • Permission check: if (!adminUser.can_manage_X) redirect("/admin/pickup").
  • Brand-scope: effectiveBrandId = brandId ?? adminUser.brand_id ?? firstBrandId.
  • Direct DB queries in pages use getActiveBrandId() helper; admin queries use pool.query with explicit WHERE brand_id = $1 or 1=1 for platform_admin.

T5 — Modal inventory

Modal Triggered from Server action
GlassModal shell many n/a (UI only)
ElegantModal shell some n/a
AdminOrdersPanel New Order modal inline fixed overlay createAdminOrder
AddStopModal AdminStopsPanel Add createStop
EditStopModal inline createStop / updateStop
NewStopForm (inline, not modal) /admin/stops/new createStop
AddLocationModal / EditLocationModal LocationsTab createLocation / updateLocation
CreateUserModal settings Users createAdminUser
CampaignListPanel New Campaign /admin/communications upsertCampaign
AbandonedCartDashboard detail abandoned carts page read-only
BrandFeatureCards Enable confirm settings Add-ons toggleBrandFeature
HeadgatesManager Edit/QR headgates page updateWaterHeadgate, regenerateHeadgateToken
Cart stop picker /cart n/a
UpgradePlanModal (lazy) DashboardClient / DashboardUpgradeButton n/a
CommandPalette global Cmd+K n/a

T6 — Edge cases inventory (extracted for Phase 3)

  • Auth/role: dev_session values, Neon Auth failures, must_change_password redirect, role-mismatched user without brand links.
  • Empty/zero states: 0 brands, 0 stops, 0 orders, 0 contacts, 0 campaigns, 0 templates, 0 wholesale customers.
  • Long data: very long product names, unicode (αβγ δεζ 🌿🍊 谢谢), special chars (<script>-style), URL-encoded emails.
  • Pagination boundaries: page 0, last page, beyond last page, single result, exactly PAGE_SIZE, 0 results.
  • Brand switching: platform_admin with no active brand, brand with no admin links, multi-brand admin.
  • Date/time: past stops, future stops, today, before cutoff, after cutoff, DST boundaries, leap years (2024 just passed).
  • Status enums: every allowed value + one disallowed value per field for boundary tests.
  • Stop states: active/paused/closed, public/private, with/without cutoff, with/without address.
  • Order states: all 4 statuses × 3 fulfillment types × with/without stop × with/without customer.
  • Order items: zero items (free order), many items, mixed pickup/ship within one order.
  • Customers: only email, only phone, both, no source, no metadata.
  • Bulk actions: 0 selected, 1 selected, all selected, partial success.
  • Network failures: timeouts, partial responses (not testable here — would need test API endpoints).
  • Image upload: missing file, oversized file, wrong type, network failure mid-upload.
  • CSV import: malformed CSV, missing columns, duplicate rows, very large file (>5 MB → bucket path).
  • Modal stacking: open modal over modal.
  • Browser back/forward: /admin/orders?new=true modal state preserved?
  • Concurrent edits: two admin tabs editing same order.
  • Offline mode: OfflineBanner state transitions + queued mutations.
  • Permission transitions: admin user disabled mid-session.

Items intentionally not enumerated in this run

  • Server actions in src/actions/ (catalogued by name in T1; full parameter signatures to be reviewed during acceptance-criteria phase).
  • JSON API route handlers (src/app/api/**/route.ts beyond auth) — 62 routes. Out of scope for UI inventory; they will be exercised indirectly via the admin UI but not unit-tested separately unless a specific bug surfaces.
  • Wholesale portal (/wholesale/*) — out of scope per user.
  • Brand storefronts (/tuxedo/*, /indian-river-direct/*) — out of scope per user.
  • Database RPCs (PL/pgSQL functions) — out of scope for inventory; exercised indirectly via server actions.
  • Background jobs (cron, email automation) — exercised via curl on their API endpoints only if a bug is suspected.