From 0a534222e840479957b92d28f0c91ec2af20481e Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 17 Jun 2026 10:18:33 -0600 Subject: [PATCH] Remove command-center page and dependencies The /admin/command-center route was platform_admin-only, used a heavy custom theme (Major Mono / JetBrains / Inter Tight + ~700 lines of schematic CSS), called three RPCs, and overlapped with the per-brand dashboards reachable at /admin, /admin/orders, /admin/stops, and /admin/reports. The page is being removed; it didn't justify the build complexity, font payload, or schema surface area. Changes: - Drop page, dashboard component, CSS, and server actions - Remove the platform_admin nav entry from header + sidebar - Remove the cmd+K palette entry - Add migration 0042 to drop the 3 RPCs, founder_pain_log table, and founder_pain_log_platform view it owned - Decrement route count in docs/pricing-assessment.md (88 -> 87) Verification: tsc clean, eslint clean, npm run build clean, command- center absent from the route list. --- db/migrations/0042_drop_command_center.sql | 19 + docs/pricing-assessment.md | 2 +- src/actions/platform/command-center.ts | 165 --- .../admin/command-center/command-center.css | 1143 ----------------- src/app/admin/command-center/page.tsx | 50 - src/components/admin/AdminHeader.tsx | 4 +- src/components/admin/AdminSidebar.tsx | 7 - .../admin/CommandCenterDashboard.tsx | 811 ------------ src/components/admin/command-palette-data.ts | 10 - 9 files changed, 21 insertions(+), 2190 deletions(-) create mode 100644 db/migrations/0042_drop_command_center.sql delete mode 100644 src/actions/platform/command-center.ts delete mode 100644 src/app/admin/command-center/command-center.css delete mode 100644 src/app/admin/command-center/page.tsx delete mode 100644 src/components/admin/CommandCenterDashboard.tsx diff --git a/db/migrations/0042_drop_command_center.sql b/db/migrations/0042_drop_command_center.sql new file mode 100644 index 0000000..88dce70 --- /dev/null +++ b/db/migrations/0042_drop_command_center.sql @@ -0,0 +1,19 @@ +-- Migration 0042: Drop command-center RPCs and founder_pain_log +-- +-- The /admin/command-center page is being removed (see commit message). +-- All objects it depended on are also dropped: +-- - get_platform_command_center_metrics (JSONB-returning RPC) +-- - get_platform_activity_feed (TABLE-returning RPC) +-- - get_brand_health_snapshot (TABLE-returning RPC) +-- - founder_pain_log (table) +-- - founder_pain_log_platform (view, joins brand name onto founder_pain_log) +-- +-- The platform_admin role has no other consumer of these objects. The +-- cross-brand KPIs and activity feed that the page showed are reachable +-- per-brand via /admin, /admin/orders, /admin/stops, and /admin/reports. + +DROP VIEW IF EXISTS founder_pain_log_platform; +DROP FUNCTION IF EXISTS get_brand_health_snapshot(); +DROP FUNCTION IF EXISTS get_platform_activity_feed(); +DROP FUNCTION IF EXISTS get_platform_command_center_metrics(); +DROP TABLE IF EXISTS founder_pain_log; diff --git a/docs/pricing-assessment.md b/docs/pricing-assessment.md index 87dc6fc..ab7bd72 100644 --- a/docs/pricing-assessment.md +++ b/docs/pricing-assessment.md @@ -12,7 +12,7 @@ A **multi-tenant B2B produce distribution platform**, not a SaaS starter. Build |---|---| | Migrations | **135 SQL files** (production schema, RLS, SECURITY DEFINER RPCs, audit logs) | | Source | **544 files** (362 .tsx + 179 .ts) | -| Admin pages | **88 routes** across orders, products, stops, wholesale, communications, time-tracking, water-log, route-trace, reports, advanced, command-center | +| Admin pages | **87 routes** across orders, products, stops, wholesale, communications, time-tracking, water-log, route-trace, reports, advanced | | API routes | **60+ endpoints** (Stripe + Square + FedEx + Resend + Twilio + 8 AI endpoints) | | Public surfaces | 2 brand storefronts (Tuxedo, Indian River Direct) with brand theming, hero, color customization | | Integrations | Stripe (Connect + subscriptions), Square (POS sync), FedEx (rates + labels), Resend (email), Twilio (SMS), OpenAI (8 AI tools) | diff --git a/src/actions/platform/command-center.ts b/src/actions/platform/command-center.ts deleted file mode 100644 index c1ff2d3..0000000 --- a/src/actions/platform/command-center.ts +++ /dev/null @@ -1,165 +0,0 @@ -"use server"; - -import { getAdminUser } from "@/lib/admin-permissions"; -import { pool } from "@/lib/db"; - -// ── Types ──────────────────────────────────────────────────────────────────── - -export type PlatformMetrics = { - active_brands: number; - orders_today: number; - revenue_today: number; - active_routes: number; - failed_orders_today: number; - pending_orders_today: number; -}; - -export type ActivityEvent = { - id: string; - event_type: string; - entity_type: string; - entity_id: string; - payload: Record; - actor_type: string; - source: string; - created_at: string; - brand_id: string | null; - brand_name: string | null; -}; - -export type BrandHealth = { - brand_id: string; - brand_name: string; - brand_slug: string; - plan_tier: string; - orders_today: number; - revenue_today: number; - active_stops: number; - failed_orders: number; - pending_orders: number; - last_activity: string | null; - open_pain_items: number; - health_status: "healthy" | "warning" | "critical"; -}; - -export type PainLogItem = { - id: string; - brand_id: string | null; - brand_name: string | null; - severity: "low" | "medium" | "high" | "critical"; - category: string; - title: string; - description: string | null; - status: string; - created_at: string; -}; - -export type CreatePainLogData = { - brand_id?: string | null; - severity: "low" | "medium" | "high" | "critical"; - category: string; - title: string; - description?: string | null; -}; - -// ── Internal RPC helper ────────────────────────────────────────────────────── - -/** - * Call a platform RPC. The action layer needs to know whether the function - * returns a scalar (JSONB) or a setof (TABLE(...)) — `SELECT fn() AS "fn"` - * only works for scalar returns; setof functions need `SELECT * FROM fn()`. - * - * - `kind: "scalar"` → wraps the single result column in a JSON value - * - `kind: "setof"` → returns the rows directly as an array - */ -async function platformRPC( - rpcName: string, - kind: "scalar" | "setof" = "scalar", -): Promise { - const adminUser = await getAdminUser(); - if (!adminUser) throw new Error("Not authenticated"); - if (adminUser.role !== "platform_admin") throw new Error("Access denied: platform admin only"); - - if (kind === "setof") { - const { rows } = await pool.query( - `SELECT * FROM ${rpcName}()`, - ); - return rows as unknown as T; - } - - // scalar: e.g. JSONB return — single cell, possibly null - const { rows } = await pool.query>( - `SELECT ${rpcName}() AS "${rpcName}"`, - ); - const data = rows[0]?.[rpcName]; - if (data == null) return null as T; - return data as T; -} - -// ── Platform actions ───────────────────────────────────────────────────────── - -export async function getPlatformMetrics(): Promise { - return platformRPC("get_platform_command_center_metrics", "scalar"); -} - -export async function getPlatformActivityFeed(): Promise { - return platformRPC("get_platform_activity_feed", "setof"); -} - -export async function getBrandHealthSnapshot(): Promise { - return platformRPC("get_brand_health_snapshot", "setof"); -} - -export async function getPainLog(): Promise { - const adminUser = await getAdminUser(); - if (!adminUser) throw new Error("Not authenticated"); - - const { rows } = await pool.query( - `SELECT * FROM founder_pain_log_platform ORDER BY created_at DESC`, - ); - - return rows; -} - -export async function createPainLogItem(data: CreatePainLogData): Promise<{ success: boolean; error?: string }> { - try { - const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (adminUser.role !== "platform_admin") return { success: false, error: "Access denied" }; - - await pool.query( - `INSERT INTO founder_pain_log (brand_id, severity, category, title, description) - VALUES ($1, $2, $3, $4, $5)`, - [ - data.brand_id || null, - data.severity, - data.category, - data.title, - data.description || null, - ], - ); - - return { success: true }; - } catch (e) { - return { success: false, error: String(e) }; - } -} - -export async function resolvePainLogItem(id: string): Promise<{ success: boolean; error?: string }> { - try { - const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (adminUser.role !== "platform_admin") return { success: false, error: "Access denied" }; - - await pool.query( - `UPDATE founder_pain_log - SET status = 'resolved', resolved_at = now(), resolved_by = $2 - WHERE id = $1`, - [id, adminUser.id], - ); - - return { success: true }; - } catch (e) { - return { success: false, error: String(e) }; - } -} diff --git a/src/app/admin/command-center/command-center.css b/src/app/admin/command-center/command-center.css deleted file mode 100644 index da6fe5f..0000000 --- a/src/app/admin/command-center/command-center.css +++ /dev/null @@ -1,1143 +0,0 @@ -/* ============================================================================ - * Command Center — Operations Schematic - * Industrial mission-control visual language. The CSS does three jobs: - * 1. Surface treatment (dot grid, scan-lines, frame ticks) - * 2. Type wiring (mono-major, mono, body fonts) - * 3. Motion (ticker, LED pulse, boot reveal) - * ========================================================================= */ - -.cc-schematic { - /* Surface palette */ - --cc-bg: #0B0B0A; - --cc-surface: #121210; - --cc-surface-2: #161614; - --cc-line: rgba(232, 228, 216, 0.08); - --cc-line-2: rgba(232, 228, 216, 0.14); - --cc-line-3: rgba(232, 228, 216, 0.22); - --cc-fg: #E8E4D8; - --cc-muted: #8B857A; - --cc-faint: #5C5852; - --cc-amber: #FFB000; - --cc-amber-dim: #B07900; - --cc-healthy: #9DB87C; - --cc-critical: #E64A2B; - --cc-info: #7CB3DB; - --cc-warn: #E0A04A; - - /* Type — fonts are injected by next/font on the page wrapper */ - --cc-font-mono-major: var(--cc-font-mono-major, "Major Mono Display"), ui-monospace, monospace; - --cc-font-mono: var(--cc-font-mono, "JetBrains Mono"), ui-monospace, SFMono-Regular, Menlo, monospace; - --cc-font-body: var(--cc-font-body, "Inter Tight"), system-ui, -apple-system, sans-serif; - - background-color: var(--cc-bg); - color: var(--cc-fg); - font-family: var(--cc-font-mono); - font-feature-settings: "ss01", "tnum", "calt" 0; - font-variant-numeric: tabular-nums; - min-height: 100vh; - position: relative; - overflow-x: hidden; -} - -/* ── Surface: dot grid + scan-lines ──────────────────────────────────────── */ -.cc-schematic::before { - /* Faint dot grid — a "graph paper" feel, not a checkered table */ - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - background-image: - radial-gradient(circle at 1px 1px, rgba(232, 228, 216, 0.05) 1px, transparent 1.5px); - background-size: 28px 28px; - z-index: 0; -} - -.cc-schematic::after { - /* Horizontal scan-line texture — barely visible, gives phosphor warmth */ - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - background-image: repeating-linear-gradient( - 0deg, - rgba(232, 228, 216, 0.012) 0px, - rgba(232, 228, 216, 0.012) 1px, - transparent 1px, - transparent 3px - ); - z-index: 0; -} - -/* Page contents need to sit above the surface textures */ -.cc-schematic > * { - position: relative; - z-index: 1; -} - -/* ── Top status bar ──────────────────────────────────────────────────────── */ -.cc-topbar { - display: grid; - grid-template-columns: auto 1fr auto; - align-items: center; - gap: 24px; - padding: 10px 24px; - border-bottom: 1px solid var(--cc-line); - font-family: var(--cc-font-mono); - font-size: 10.5px; - letter-spacing: 0.16em; - text-transform: uppercase; - color: var(--cc-muted); - background: - linear-gradient(180deg, rgba(255, 176, 0, 0.02) 0%, transparent 100%), - var(--cc-bg); -} - -.cc-topbar__brand { - display: flex; - align-items: center; - gap: 12px; - color: var(--cc-fg); -} -.cc-topbar__brand-mark { - color: var(--cc-amber); - font-family: var(--cc-font-mono-major); - font-size: 11px; - letter-spacing: 0; -} -.cc-topbar__sep { color: var(--cc-faint); } -.cc-topbar__scope { color: var(--cc-muted); } -.cc-topbar__scope b { color: var(--cc-fg); font-weight: 600; } - -.cc-ticker { - position: relative; - overflow: hidden; - white-space: nowrap; - border-left: 1px solid var(--cc-line); - border-right: 1px solid var(--cc-line); - padding: 0 16px; - mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); - -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); -} -.cc-ticker__track { - display: inline-block; - white-space: nowrap; - animation: cc-ticker 60s linear infinite; - color: var(--cc-faint); -} -.cc-ticker__chip { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 0 18px; -} -.cc-ticker__chip::before { - content: "▸"; - color: var(--cc-amber); - font-size: 9px; -} -.cc-ticker__chip b { - color: var(--cc-fg); - font-weight: 500; -} -.cc-ticker__chip span { - color: var(--cc-faint); - text-transform: none; - letter-spacing: 0.04em; -} - -.cc-topbar__meta { - display: flex; - align-items: center; - gap: 18px; -} -.cc-topbar__clock { - color: var(--cc-fg); - font-variant-numeric: tabular-nums; -} - -/* ── LED pulse ──────────────────────────────────────────────────────────── */ -.cc-led { - display: inline-block; - width: 6px; - height: 6px; - border-radius: 50%; - background: currentColor; - position: relative; - flex-shrink: 0; -} -.cc-led::after { - /* Soft glow halo */ - content: ""; - position: absolute; - inset: -3px; - border-radius: 50%; - background: currentColor; - opacity: 0.25; - filter: blur(3px); -} -.cc-led--live { - color: var(--cc-amber); - animation: cc-led-pulse 1.6s ease-in-out infinite; -} -.cc-led--healthy { color: var(--cc-healthy); } -.cc-led--warn { color: var(--cc-warn); } -.cc-led--critical{ color: var(--cc-critical); } -.cc-led--info { color: var(--cc-info); } -.cc-led--off { color: var(--cc-faint); } - -@keyframes cc-led-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } -} - -/* ── Page frame ticks (decorative column rules) ──────────────────────────── */ -.cc-frame { - position: relative; - max-width: 1600px; - margin: 0 auto; - padding: 32px 32px 64px; -} - -.cc-frame::before, -.cc-frame::after { - /* Tick marks running down the side rails, like a precision instrument */ - content: ""; - position: absolute; - top: 32px; - bottom: 64px; - width: 1px; - background-image: repeating-linear-gradient( - 180deg, - var(--cc-line-2) 0, - var(--cc-line-2) 2px, - transparent 2px, - transparent 12px - ); -} -.cc-frame::before { left: 12px; } -.cc-frame::after { right: 12px; } - -/* ── Header / masthead ───────────────────────────────────────────────────── */ -.cc-masthead { - display: grid; - grid-template-columns: 1fr auto; - align-items: end; - gap: 32px; - padding-bottom: 18px; - border-bottom: 1px solid var(--cc-line-2); - position: relative; -} -.cc-masthead::after { - /* Decorative tick scale above the rule */ - content: ""; - position: absolute; - left: 0; - right: 0; - bottom: -1px; - height: 4px; - background-image: repeating-linear-gradient( - 90deg, - var(--cc-amber) 0, - var(--cc-amber) 1px, - transparent 1px, - transparent 24px - ); - opacity: 0.5; -} - -.cc-masthead__eyebrow { - display: flex; - align-items: center; - gap: 10px; - font-family: var(--cc-font-mono); - font-size: 10.5px; - letter-spacing: 0.28em; - text-transform: uppercase; - color: var(--cc-amber); - margin-bottom: 14px; -} -.cc-masthead__eyebrow::before { - content: "//"; - color: var(--cc-faint); -} -.cc-masthead__title { - font-family: var(--cc-font-mono-major); - font-size: clamp(40px, 5.5vw, 72px); - line-height: 0.92; - letter-spacing: -0.01em; - color: var(--cc-fg); - text-transform: uppercase; -} -.cc-masthead__title em { - font-style: normal; - color: var(--cc-amber); - text-shadow: 0 0 24px rgba(255, 176, 0, 0.25); -} -.cc-masthead__sub { - margin-top: 16px; - font-family: var(--cc-font-body); - font-size: 13px; - line-height: 1.55; - color: var(--cc-muted); - max-width: 56ch; -} -.cc-masthead__sub b { - color: var(--cc-fg); - font-weight: 500; - font-family: var(--cc-font-mono); - font-size: 11.5px; - letter-spacing: 0.04em; - background: rgba(232, 228, 216, 0.04); - padding: 2px 6px; - border: 1px solid var(--cc-line); -} - -.cc-masthead__actions { - display: flex; - align-items: center; - gap: 10px; -} - -/* ── Buttons (schematic) ─────────────────────────────────────────────────── */ -.cc-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 9px 14px; - font-family: var(--cc-font-mono); - font-size: 10.5px; - font-weight: 500; - letter-spacing: 0.16em; - text-transform: uppercase; - color: var(--cc-fg); - background: var(--cc-surface); - border: 1px solid var(--cc-line-2); - border-radius: 0; - cursor: pointer; - transition: background 140ms ease, border-color 140ms ease, color 140ms ease; -} -.cc-btn:hover { - background: var(--cc-surface-2); - border-color: var(--cc-line-3); -} -.cc-btn--primary { - background: var(--cc-amber); - color: #1A1408; - border-color: var(--cc-amber); -} -.cc-btn--primary:hover { - background: #FFC233; - border-color: #FFC233; -} -.cc-btn--ghost { - background: transparent; - border-color: var(--cc-line); - color: var(--cc-muted); -} -.cc-btn--ghost:hover { - color: var(--cc-fg); - border-color: var(--cc-line-2); -} -.cc-btn--resolved { - background: rgba(157, 184, 124, 0.12); - border-color: rgba(157, 184, 124, 0.4); - color: var(--cc-healthy); -} -.cc-btn--resolved:hover { - background: rgba(157, 184, 124, 0.2); -} -.cc-btn--snooze { - background: transparent; - border-color: var(--cc-line); - color: var(--cc-muted); -} -.cc-btn--snooze:hover { color: var(--cc-fg); } - -/* ── Section header (serial + title) ────────────────────────────────────── */ -.cc-section { - margin-top: 40px; -} -.cc-section__head { - display: grid; - grid-template-columns: auto 1fr auto; - align-items: center; - gap: 16px; - margin-bottom: 18px; - font-family: var(--cc-font-mono); - font-size: 10.5px; - letter-spacing: 0.28em; - text-transform: uppercase; - color: var(--cc-muted); -} -.cc-section__serial { - color: var(--cc-amber); - font-weight: 600; -} -.cc-section__title { - color: var(--cc-fg); - font-weight: 500; -} -.cc-section__title::before { - content: "// "; - color: var(--cc-faint); -} -.cc-section__rule { - height: 1px; - background: var(--cc-line); - position: relative; -} -.cc-section__rule::before, -.cc-section__rule::after { - content: ""; - position: absolute; - top: -1px; - width: 1px; - height: 3px; - background: var(--cc-amber); -} -.cc-section__rule::before { left: 0; } -.cc-section__rule::after { right: 0; } -.cc-section__count { - font-variant-numeric: tabular-nums; - color: var(--cc-fg); - background: var(--cc-surface); - border: 1px solid var(--cc-line); - padding: 3px 8px; -} - -/* ── KPI grid ────────────────────────────────────────────────────────────── */ -.cc-kpi-grid { - display: grid; - grid-template-columns: repeat(6, 1fr); - gap: 1px; - background: var(--cc-line); - border: 1px solid var(--cc-line-2); -} -@media (max-width: 1100px) { .cc-kpi-grid { grid-template-columns: repeat(3, 1fr); } } -@media (max-width: 640px) { .cc-kpi-grid { grid-template-columns: repeat(2, 1fr); } } - -.cc-kpi { - position: relative; - background: var(--cc-surface); - padding: 20px 20px 18px; - min-height: 156px; - display: flex; - flex-direction: column; - gap: 12px; - transition: background 160ms ease; -} -.cc-kpi:hover { background: var(--cc-surface-2); } -.cc-kpi__head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} -.cc-kpi__label { - font-family: var(--cc-font-mono); - font-size: 9.5px; - letter-spacing: 0.22em; - text-transform: uppercase; - color: var(--cc-muted); - font-weight: 500; -} -.cc-kpi__serial { - font-family: var(--cc-font-mono); - font-size: 9.5px; - letter-spacing: 0.1em; - color: var(--cc-faint); -} -.cc-kpi__value { - display: flex; - align-items: baseline; - gap: 6px; - font-family: var(--cc-font-mono); - font-variant-numeric: tabular-nums; - font-weight: 600; - font-size: 38px; - line-height: 1; - color: var(--cc-fg); - letter-spacing: -0.02em; -} -.cc-kpi__value--critical { color: var(--cc-critical); } -.cc-kpi__value--warn { color: var(--cc-warn); } -.cc-kpi__value--healthy { color: var(--cc-fg); } -.cc-kpi__unit { - font-family: var(--cc-font-mono); - font-size: 13px; - color: var(--cc-faint); - font-weight: 400; - letter-spacing: 0.04em; -} -.cc-kpi__sub { - font-family: var(--cc-font-mono); - font-size: 10.5px; - letter-spacing: 0.06em; - color: var(--cc-muted); - display: flex; - align-items: center; - gap: 6px; -} -.cc-kpi__spark { - display: flex; - align-items: flex-end; - gap: 2px; - height: 22px; - margin-top: auto; -} -.cc-kpi__bar { - flex: 1; - background: var(--cc-line-2); - min-height: 2px; - transition: background 200ms ease; -} -.cc-kpi__bar--lit { background: var(--cc-amber); } -.cc-kpi__bar--critical { background: var(--cc-critical); } -.cc-kpi__bar--healthy { background: var(--cc-healthy); } -.cc-kpi__bar--warn { background: var(--cc-warn); } - -/* Corner crosshairs (CAD-style) */ -.cc-corner { - position: absolute; - width: 10px; - height: 10px; - pointer-events: none; -} -.cc-corner::before, .cc-corner::after { - content: ""; - position: absolute; - background: var(--cc-line-3); -} -.cc-corner--tl { top: 4px; left: 4px; } -.cc-corner--tl::before { top: 0; left: 0; width: 6px; height: 1px; } -.cc-corner--tl::after { top: 0; left: 0; width: 1px; height: 6px; } -.cc-corner--tr { top: 4px; right: 4px; } -.cc-corner--tr::before { top: 0; right: 0; width: 6px; height: 1px; } -.cc-corner--tr::after { top: 0; right: 0; width: 1px; height: 6px; } -.cc-corner--bl { bottom: 4px; left: 4px; } -.cc-corner--bl::before { bottom: 0; left: 0; width: 6px; height: 1px; } -.cc-corner--bl::after { bottom: 0; left: 0; width: 1px; height: 6px; } -.cc-corner--br { bottom: 4px; right: 4px; } -.cc-corner--br::before { bottom: 0; right: 0; width: 6px; height: 1px; } -.cc-corner--br::after { bottom: 0; right: 0; width: 1px; height: 6px; } - -/* ── Briefing (AI readout panel) ─────────────────────────────────────────── */ -.cc-briefing { - position: relative; - background: var(--cc-surface); - border: 1px solid var(--cc-line-2); - padding: 24px 28px; - display: grid; - grid-template-columns: 1fr auto; - gap: 24px; -} -.cc-briefing__main { min-width: 0; } -.cc-briefing__head { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 16px; - font-family: var(--cc-font-mono); - font-size: 10.5px; - letter-spacing: 0.24em; - text-transform: uppercase; - color: var(--cc-amber); -} -.cc-briefing__head::before { - content: "▣"; - font-size: 13px; - letter-spacing: 0; - text-shadow: 0 0 8px rgba(255, 176, 0, 0.4); -} -.cc-briefing__head-meta { - margin-left: auto; - display: flex; - align-items: center; - gap: 8px; - color: var(--cc-faint); - font-size: 9.5px; - letter-spacing: 0.18em; -} -.cc-briefing__lines { - display: grid; - gap: 10px; -} -.cc-briefing__line { - display: grid; - grid-template-columns: 28px 1fr; - gap: 12px; - align-items: baseline; - font-family: var(--cc-font-mono); - font-size: 13px; - line-height: 1.6; - color: var(--cc-fg); -} -.cc-briefing__line-idx { - color: var(--cc-amber); - font-weight: 600; - font-size: 11px; - letter-spacing: 0.08em; - font-variant-numeric: tabular-nums; -} -.cc-briefing__line-idx::after { - content: "│"; - margin-left: 4px; - color: var(--cc-faint); -} -.cc-briefing__line b { - color: var(--cc-amber); - font-weight: 600; -} -.cc-briefing__line em { - color: var(--cc-critical); - font-style: normal; - font-weight: 600; -} -.cc-briefing__line-ok { - color: var(--cc-healthy); -} -.cc-briefing__model { - align-self: start; - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 6px; - padding: 8px 12px; - border: 1px solid var(--cc-line); - font-family: var(--cc-font-mono); - font-size: 9.5px; - letter-spacing: 0.2em; - text-transform: uppercase; - color: var(--cc-muted); - min-width: 132px; -} -.cc-briefing__model b { - color: var(--cc-fg); - font-weight: 600; -} -.cc-briefing__model-row { - display: flex; - justify-content: space-between; - gap: 12px; - width: 100%; -} -.cc-briefing__model-row b { color: var(--cc-amber); font-weight: 500; } - -/* ── Two-column body ─────────────────────────────────────────────────────── */ -.cc-grid { - display: grid; - grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); - gap: 32px; -} -@media (max-width: 1024px) { .cc-grid { grid-template-columns: 1fr; } } - -/* ── Brand panel (LEFT column) ───────────────────────────────────────────── */ -.cc-brand { - position: relative; - background: var(--cc-surface); - border: 1px solid var(--cc-line-2); - padding: 22px 24px 20px; - display: grid; - grid-template-columns: 1fr auto; - gap: 20px; - align-items: start; - transition: background 160ms ease, border-color 160ms ease; -} -.cc-brand:hover { background: var(--cc-surface-2); } -.cc-brand--critical { border-color: rgba(230, 74, 43, 0.35); } -.cc-brand--warning { border-color: rgba(224, 160, 74, 0.3); } -.cc-brand--healthy { border-color: rgba(157, 184, 124, 0.18); } - -.cc-brand__head { min-width: 0; } -.cc-brand__name { - display: flex; - align-items: center; - gap: 10px; - font-family: var(--cc-font-mono); - font-size: 18px; - font-weight: 600; - letter-spacing: -0.01em; - color: var(--cc-fg); - line-height: 1.1; -} -.cc-brand__slug { - font-family: var(--cc-font-mono); - font-size: 11px; - letter-spacing: 0.04em; - color: var(--cc-faint); - margin-top: 6px; -} -.cc-brand__slug b { - color: var(--cc-muted); - font-weight: 500; -} -.cc-brand__status { - display: inline-flex; - align-items: center; - gap: 6px; - font-family: var(--cc-font-mono); - font-size: 9.5px; - letter-spacing: 0.22em; - text-transform: uppercase; - font-weight: 600; - padding: 5px 9px; - border: 1px solid; -} -.cc-brand__status--healthy { color: var(--cc-healthy); border-color: rgba(157, 184, 124, 0.35); background: rgba(157, 184, 124, 0.05); } -.cc-brand__status--warning { color: var(--cc-warn); border-color: rgba(224, 160, 74, 0.35); background: rgba(224, 160, 74, 0.05); } -.cc-brand__status--critical { color: var(--cc-critical); border-color: rgba(230, 74, 43, 0.35); background: rgba(230, 74, 43, 0.05); } - -.cc-brand__metrics { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 0; - border-top: 1px solid var(--cc-line); - border-bottom: 1px solid var(--cc-line); - margin: 18px 0 0; - padding: 12px 0; - grid-column: 1 / -1; -} -.cc-brand__metric { - position: relative; - padding: 0 16px; - display: flex; - flex-direction: column; - gap: 4px; -} -.cc-brand__metric + .cc-brand__metric::before { - content: ""; - position: absolute; - top: 4px; - bottom: 4px; - left: 0; - width: 1px; - background: var(--cc-line); -} -.cc-brand__metric-label { - font-family: var(--cc-font-mono); - font-size: 9.5px; - letter-spacing: 0.22em; - text-transform: uppercase; - color: var(--cc-faint); -} -.cc-brand__metric-value { - font-family: var(--cc-font-mono); - font-variant-numeric: tabular-nums; - font-size: 22px; - font-weight: 600; - color: var(--cc-fg); - letter-spacing: -0.01em; -} -.cc-brand__metric-value--critical { color: var(--cc-critical); } - -.cc-brand__pain { - grid-column: 1 / -1; - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: rgba(230, 74, 43, 0.06); - border: 1px solid rgba(230, 74, 43, 0.3); - margin-top: 14px; - font-family: var(--cc-font-mono); - font-size: 11px; - letter-spacing: 0.06em; - color: var(--cc-critical); -} -.cc-brand__pain b { color: var(--cc-critical); font-weight: 700; } -.cc-brand__pain span { color: var(--cc-muted); margin-left: auto; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; } - -/* ── Pain log item (RIGHT column) ────────────────────────────────────────── */ -.cc-pain { - position: relative; - background: var(--cc-surface); - border: 1px solid var(--cc-line-2); - padding: 14px 16px; - display: grid; - gap: 10px; - transition: background 140ms ease, border-color 140ms ease; -} -.cc-pain:hover { background: var(--cc-surface-2); } -.cc-pain--critical { border-color: rgba(230, 74, 43, 0.35); } -.cc-pain--high { border-color: rgba(224, 160, 74, 0.3); } -.cc-pain--medium { border-color: rgba(255, 176, 0, 0.25); } - -.cc-pain__head { - display: grid; - grid-template-columns: auto 1fr auto; - align-items: center; - gap: 12px; -} -.cc-pain__severity { - display: inline-flex; - align-items: center; - gap: 6px; - font-family: var(--cc-font-mono); - font-size: 9.5px; - letter-spacing: 0.22em; - text-transform: uppercase; - font-weight: 600; -} -.cc-pain__title { - font-family: var(--cc-font-mono); - font-size: 13px; - font-weight: 500; - color: var(--cc-fg); - line-height: 1.4; - letter-spacing: -0.005em; -} -.cc-pain__actions { display: flex; gap: 6px; } -.cc-pain__foot { - display: flex; - align-items: center; - gap: 10px; - font-family: var(--cc-font-mono); - font-size: 10px; - letter-spacing: 0.1em; - color: var(--cc-faint); - text-transform: uppercase; -} -.cc-pain__foot b { color: var(--cc-muted); font-weight: 500; } -.cc-pain__foot-sep { color: var(--cc-line-3); } - -/* ── Add-issue form (right column) ───────────────────────────────────────── */ -.cc-form { - background: var(--cc-surface); - border: 1px solid var(--cc-line-2); - padding: 18px; - display: grid; - gap: 10px; -} -.cc-form__head { - display: flex; - align-items: center; - gap: 10px; - font-family: var(--cc-font-mono); - font-size: 10.5px; - letter-spacing: 0.24em; - text-transform: uppercase; - color: var(--cc-amber); - margin-bottom: 6px; -} -.cc-form__head::before { - content: "//"; - color: var(--cc-faint); -} -.cc-field { - width: 100%; - padding: 10px 12px; - background: var(--cc-bg); - border: 1px solid var(--cc-line-2); - color: var(--cc-fg); - font-family: var(--cc-font-mono); - font-size: 12px; - letter-spacing: 0.02em; - border-radius: 0; - outline: none; - transition: border-color 140ms ease, background 140ms ease; -} -.cc-field::placeholder { color: var(--cc-faint); font-style: normal; } -.cc-field:focus { border-color: var(--cc-amber); background: var(--cc-surface-2); } -select.cc-field { - appearance: none; - background-image: linear-gradient(45deg, transparent 50%, var(--cc-muted) 50%), - linear-gradient(135deg, var(--cc-muted) 50%, transparent 50%); - background-position: calc(100% - 16px) 50%, calc(100% - 11px) 50%; - background-size: 5px 5px, 5px 5px; - background-repeat: no-repeat; - padding-right: 32px; -} -select.cc-field option { background: var(--cc-surface); color: var(--cc-fg); } -.cc-form__row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } -.cc-form__actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 4px; } - -/* ── Activity feed ───────────────────────────────────────────────────────── */ -.cc-feed { - background: var(--cc-surface); - border: 1px solid var(--cc-line-2); -} -.cc-feed__list { - max-height: 460px; - overflow-y: auto; - padding: 4px 0; -} -.cc-feed__row { - display: grid; - grid-template-columns: 80px auto 1fr auto; - align-items: center; - gap: 14px; - padding: 11px 20px; - font-family: var(--cc-font-mono); - font-size: 11.5px; - color: var(--cc-fg); - border-bottom: 1px solid var(--cc-line); - transition: background 120ms ease; -} -.cc-feed__row:last-child { border-bottom: 0; } -.cc-feed__row:hover { background: var(--cc-surface-2); } -.cc-feed__time { - color: var(--cc-faint); - font-variant-numeric: tabular-nums; - font-size: 10.5px; - letter-spacing: 0.04em; -} -.cc-feed__code { - font-family: var(--cc-font-mono); - font-size: 9.5px; - font-weight: 600; - letter-spacing: 0.16em; - text-transform: uppercase; - padding: 3px 7px; - border: 1px solid; - white-space: nowrap; -} -.cc-feed__code--order_placed { color: #B79CFF; border-color: rgba(183, 156, 255, 0.3); background: rgba(183, 156, 255, 0.05); } -.cc-feed__code--order_completed { color: var(--cc-healthy); border-color: rgba(157, 184, 124, 0.3); background: rgba(157, 184, 124, 0.05); } -.cc-feed__code--pickup_completed { color: var(--cc-info); border-color: rgba(124, 179, 219, 0.3); background: rgba(124, 179, 219, 0.05); } -.cc-feed__code--payment_failed { color: var(--cc-critical); border-color: rgba(230, 74, 43, 0.35); background: rgba(230, 74, 43, 0.06); } -.cc-feed__code--stop_created { color: var(--cc-warn); border-color: rgba(224, 160, 74, 0.3); background: rgba(224, 160, 74, 0.05); } -.cc-feed__code--route_updated { color: var(--cc-info); border-color: rgba(124, 179, 219, 0.3); background: rgba(124, 179, 219, 0.05); } -.cc-feed__code--default { color: var(--cc-muted); border-color: var(--cc-line-2); background: var(--cc-surface-2); } - -.cc-feed__detail { min-width: 0; display: flex; align-items: baseline; gap: 8px; } -.cc-feed__brand { color: var(--cc-muted); font-size: 11px; } -.cc-feed__entity { color: var(--cc-fg); font-size: 11.5px; } -.cc-feed__entity-id { - color: var(--cc-faint); - font-size: 10.5px; - font-family: var(--cc-font-mono); - font-variant-numeric: tabular-nums; -} -.cc-feed__ago { - color: var(--cc-faint); - font-size: 10.5px; - font-variant-numeric: tabular-nums; - white-space: nowrap; -} - -/* ── Quick Access (links) ────────────────────────────────────────────────── */ -.cc-links { - background: var(--cc-surface); - border: 1px solid var(--cc-line-2); -} -.cc-link { - display: grid; - grid-template-columns: 28px 1fr auto; - align-items: center; - gap: 12px; - padding: 12px 18px; - font-family: var(--cc-font-mono); - font-size: 11.5px; - letter-spacing: 0.04em; - color: var(--cc-fg); - text-decoration: none; - border-bottom: 1px solid var(--cc-line); - transition: background 140ms ease, color 140ms ease, padding-left 200ms ease; -} -.cc-link:last-child { border-bottom: 0; } -.cc-link:hover { - background: var(--cc-surface-2); - color: var(--cc-amber); - padding-left: 22px; -} -.cc-link__serial { - color: var(--cc-faint); - font-size: 10px; - letter-spacing: 0.1em; -} -.cc-link__label { font-weight: 500; } -.cc-link__arrow { - color: var(--cc-faint); - font-family: var(--cc-font-mono); - font-size: 14px; -} -.cc-link:hover .cc-link__arrow { color: var(--cc-amber); } - -/* ── Empty state ─────────────────────────────────────────────────────────── */ -.cc-empty { - background: var(--cc-surface); - border: 1px dashed var(--cc-line-2); - padding: 48px 24px; - text-align: center; - font-family: var(--cc-font-mono); - color: var(--cc-muted); -} -.cc-empty__title { - color: var(--cc-fg); - font-size: 14px; - font-weight: 600; - letter-spacing: 0.04em; -} -.cc-empty__sub { - margin-top: 8px; - font-size: 11px; - color: var(--cc-faint); - letter-spacing: 0.08em; -} - -/* ── Skeleton (loading) ──────────────────────────────────────────────────── */ -.cc-skeleton { - background: var(--cc-surface); - border: 1px solid var(--cc-line-2); - padding: 14px 18px; - font-family: var(--cc-font-mono); - font-size: 11px; - color: var(--cc-muted); - display: flex; - align-items: center; - gap: 12px; - letter-spacing: 0.16em; - text-transform: uppercase; - position: relative; - overflow: hidden; -} -.cc-skeleton__bar { - display: inline-block; - font-family: var(--cc-font-mono); - font-size: 14px; - letter-spacing: 0; - color: var(--cc-amber); - text-shadow: 0 0 8px rgba(255, 176, 0, 0.4); - animation: cc-blink 1.1s steps(2) infinite; -} -.cc-skeleton__bar span { color: var(--cc-faint); } -@keyframes cc-blink { - 0%, 49% { opacity: 1; } - 50%, 100% { opacity: 0.35; } -} - -/* ── Error state ("Connection Lost" → "Signal Lost") ─────────────────────── */ -.cc-signal-lost { - min-height: 100vh; - display: grid; - place-items: center; - padding: 40px 24px; -} -.cc-signal-lost__panel { - position: relative; - background: var(--cc-surface); - border: 1px solid rgba(230, 74, 43, 0.35); - padding: 44px 48px; - max-width: 560px; - width: 100%; - font-family: var(--cc-font-mono); -} -.cc-signal-lost__eyebrow { - display: flex; - align-items: center; - gap: 10px; - font-size: 10.5px; - letter-spacing: 0.28em; - text-transform: uppercase; - color: var(--cc-critical); - margin-bottom: 18px; -} -.cc-signal-lost__eyebrow::before { - content: "●"; - font-size: 9px; - animation: cc-led-pulse 1.2s ease-in-out infinite; - text-shadow: 0 0 8px var(--cc-critical); -} -.cc-signal-lost__title { - font-family: var(--cc-font-mono-major); - font-size: 44px; - line-height: 0.95; - letter-spacing: -0.01em; - color: var(--cc-fg); - text-transform: uppercase; -} -.cc-signal-lost__title em { - font-style: normal; - color: var(--cc-critical); -} -.cc-signal-lost__sub { - margin-top: 16px; - font-family: var(--cc-font-body); - font-size: 13.5px; - line-height: 1.6; - color: var(--cc-muted); -} -.cc-signal-lost__error { - margin-top: 22px; - padding: 12px 14px; - background: var(--cc-bg); - border: 1px solid var(--cc-line); - border-left: 2px solid var(--cc-critical); - font-size: 11.5px; - color: var(--cc-muted); - letter-spacing: 0.02em; - white-space: pre-wrap; - word-break: break-word; -} -.cc-signal-lost__actions { - margin-top: 24px; - display: flex; - align-items: center; - gap: 12px; -} -.cc-signal-lost__hint { - font-family: var(--cc-font-mono); - font-size: 9.5px; - letter-spacing: 0.18em; - text-transform: uppercase; - color: var(--cc-faint); - margin-top: 22px; - padding-top: 18px; - border-top: 1px solid var(--cc-line); - display: flex; - justify-content: space-between; -} - -/* ── Ticker animation ───────────────────────────────────────────────────── */ -@keyframes cc-ticker { - from { transform: translateX(0); } - to { transform: translateX(-50%); } -} - -/* ── Boot reveal (stagger) ───────────────────────────────────────────────── */ -@keyframes cc-rise { - from { opacity: 0; transform: translateY(6px); } - to { opacity: 1; transform: translateY(0); } -} -.cc-rise { - animation: cc-rise 320ms cubic-bezier(0.2, 0.6, 0.2, 1) both; -} -.cc-rise[data-stagger="1"] { animation-delay: 30ms; } -.cc-rise[data-stagger="2"] { animation-delay: 60ms; } -.cc-rise[data-stagger="3"] { animation-delay: 90ms; } -.cc-rise[data-stagger="4"] { animation-delay: 120ms; } -.cc-rise[data-stagger="5"] { animation-delay: 150ms; } -.cc-rise[data-stagger="6"] { animation-delay: 180ms; } -.cc-rise[data-stagger="7"] { animation-delay: 210ms; } -.cc-rise[data-stagger="8"] { animation-delay: 240ms; } -.cc-rise[data-stagger="9"] { animation-delay: 270ms; } -.cc-rise[data-stagger="10"] { animation-delay: 300ms; } - -/* ── Scrollbar (schematic) ───────────────────────────────────────────────── */ -.cc-feed__list::-webkit-scrollbar { width: 6px; } -.cc-feed__list::-webkit-scrollbar-track { background: transparent; } -.cc-feed__list::-webkit-scrollbar-thumb { - background: var(--cc-line-2); - border-radius: 0; -} -.cc-feed__list::-webkit-scrollbar-thumb:hover { background: var(--cc-line-3); } - -/* ── Reduce motion ───────────────────────────────────────────────────────── */ -@media (prefers-reduced-motion: reduce) { - .cc-ticker__track, - .cc-led--live, - .cc-skeleton__bar, - .cc-signal-lost__eyebrow::before, - .cc-rise { - animation: none !important; - } - .cc-rise { opacity: 1 !important; transform: none !important; } -} - -/* ── TV Mode (oversized for wall display) ────────────────────────────────── */ -.cc-schematic[data-tv="true"] .cc-kpi__value { font-size: 56px; } -.cc-schematic[data-tv="true"] .cc-masthead__title { font-size: clamp(56px, 7vw, 96px); } -.cc-schematic[data-tv="true"] .cc-frame { max-width: 1840px; } -.cc-schematic[data-tv="true"] .cc-brand__name { font-size: 22px; } -.cc-schematic[data-tv="true"] .cc-briefing { padding: 32px 40px; } -.cc-schematic[data-tv="true"] .cc-briefing__line { font-size: 15px; } diff --git a/src/app/admin/command-center/page.tsx b/src/app/admin/command-center/page.tsx deleted file mode 100644 index fd70aaa..0000000 --- a/src/app/admin/command-center/page.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { getAdminUser } from "@/lib/admin-permissions"; -import AdminAccessDenied from "@/components/admin/AdminAccessDenied"; -import CommandCenterDashboard from "@/components/admin/CommandCenterDashboard"; -import { redirect } from "next/navigation"; -import { Major_Mono_Display, JetBrains_Mono, Inter_Tight } from "next/font/google"; -import "./command-center.css"; - -/** - * Command Center — Operations Schematic - * - * Industrial mission-control feel: phosphor amber on near-black, dense data, - * CAD-style corner crosshairs, brand ticker, serial-numbered sections. Three - * typefaces working in concert: Major Mono Display (wordmark only), JetBrains - * Mono (all labels/numbers/timestamps), Inter Tight (descriptive body copy). - */ -const majorMono = Major_Mono_Display({ - subsets: ["latin"], - weight: "400", - variable: "--cc-font-mono-major", - display: "swap", -}); - -const jetbrainsMono = JetBrains_Mono({ - subsets: ["latin"], - variable: "--cc-font-mono", - display: "swap", - weight: ["400", "500", "600", "700"], -}); - -const interTight = Inter_Tight({ - subsets: ["latin"], - variable: "--cc-font-body", - display: "swap", - weight: ["400", "500", "600"], -}); - -export default async function CommandCenterPage() { - const adminUser = await getAdminUser(); - if (!adminUser) return ; - if (adminUser.role !== "platform_admin") return ; - if (adminUser.must_change_password) redirect("/change-password"); - - return ( -
- -
- ); -} diff --git a/src/components/admin/AdminHeader.tsx b/src/components/admin/AdminHeader.tsx index 05a2790..20d9b3e 100644 --- a/src/components/admin/AdminHeader.tsx +++ b/src/components/admin/AdminHeader.tsx @@ -80,9 +80,7 @@ export default function AdminHeader({ userRole, canManageUsers, routeTraceEnable ? [{ href: "/admin/route-trace", label: "Route Trace" }] : []; - const fullNavLinks = BASE_NAV_LINKS.concat( - userRole === "platform_admin" ? [{ href: "/admin/command-center", label: "Command Center" }] : [] - ).concat(moduleLinks); + const fullNavLinks = BASE_NAV_LINKS.concat(moduleLinks); const navLinks = isStoreEmployee ? EMPLOYEE_NAV_LINKS : fullNavLinks; const homeHref = isStoreEmployee ? "/admin/wholesale" : "/admin"; diff --git a/src/components/admin/AdminSidebar.tsx b/src/components/admin/AdminSidebar.tsx index 96e1fa9..c2e0aae 100644 --- a/src/components/admin/AdminSidebar.tsx +++ b/src/components/admin/AdminSidebar.tsx @@ -12,7 +12,6 @@ import { Ship, Mail, Store, - Sparkles, Upload, BrainCircuit, Clock, @@ -74,12 +73,6 @@ const NAV_GROUPS: NavGroup[] = [ label: "Workspace", items: [ { href: "/admin", label: "Dashboard", icon: LayoutDashboard }, - { - href: "/admin/command-center", - label: "Command Center", - icon: Sparkles, - requiresPlatformAdmin: true, - }, ], }, { diff --git a/src/components/admin/CommandCenterDashboard.tsx b/src/components/admin/CommandCenterDashboard.tsx deleted file mode 100644 index 5bfad15..0000000 --- a/src/components/admin/CommandCenterDashboard.tsx +++ /dev/null @@ -1,811 +0,0 @@ -"use client"; -/* eslint-disable react-hooks/set-state-in-effect */ - -import { useState, useEffect, useCallback, useMemo } from "react"; -import { - getPlatformMetrics, - getPlatformActivityFeed, - getBrandHealthSnapshot, - getPainLog, - resolvePainLogItem, - createPainLogItem, - type PlatformMetrics, - type ActivityEvent, - type BrandHealth, - type PainLogItem, -} from "@/actions/platform/command-center"; - -// ── Constants ────────────────────────────────────────────────────────────── - -const REFRESH_INTERVAL = 30000; - -const SEVERITY_LED: Record = { - critical: "cc-led--critical", - high: "cc-led--warn", - medium: "cc-led cc-led--info", - low: "cc-led--off", -}; - -const HEALTH_LED: Record = { - healthy: "cc-led--healthy", - warning: "cc-led--warn", - critical: "cc-led--critical", -}; - -const HEALTH_CLASS: Record = { - healthy: "cc-brand--healthy", - warning: "cc-brand--warning", - critical: "cc-brand--critical", -}; - -const HEALTH_LABEL: Record = { - healthy: "OPERATIONAL", - warning: "ATTENTION", - critical: "CRITICAL", -}; - -const FEED_CODE_CLASS: Record = { - order_placed: "cc-feed__code--order_placed", - order_completed: "cc-feed__code--order_completed", - pickup_completed: "cc-feed__code--pickup_completed", - payment_failed: "cc-feed__code--payment_failed", - stop_created: "cc-feed__code--stop_created", - route_updated: "cc-feed__code--route_updated", -}; - -const QUICK_ACCESS = [ - { label: "Reports", href: "/admin/reports" }, - { label: "Orders", href: "/admin/orders" }, - { label: "Stops", href: "/admin/stops" }, - { label: "Settings", href: "/admin/settings" }, -]; - -// ── Helpers ──────────────────────────────────────────────────────────────── - -function formatCurrency(n: number): string { - return new Intl.NumberFormat("en-US", { - style: "currency", currency: "USD", - minimumFractionDigits: 0, maximumFractionDigits: 0, - }).format(n); -} - -function formatCurrencyFull(n: number): string { - return new Intl.NumberFormat("en-US", { - style: "currency", currency: "USD", - minimumFractionDigits: 2, maximumFractionDigits: 2, - }).format(n); -} - -function timeAgo(dateStr: string): string { - const diff = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000); - if (diff < 60) return `${diff}s ago`; - if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; - if (diff < 86400)return `${Math.floor(diff / 3600)}h ago`; - return `${Math.floor(diff / 86400)}d ago`; -} - -function formatTime(dateStr: string): string { - const d = new Date(dateStr); - return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }); -} - -function formatToday(): string { - return new Date().toLocaleDateString("en-US", { - weekday: "short", month: "short", day: "2-digit", year: "numeric", - }).toUpperCase(); -} - -function formatClock(): string { - return new Date().toLocaleTimeString("en-US", { - hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", - }); -} - -/** - * Stable pseudo-random sparkline series. Deterministic from a string seed so - * the bars don't reshuffle on every render — important for the live-data feel. - * 12 bars, values 0.1–1.0. - */ -function spark(seed: string, len = 12): number[] { - const out: number[] = []; - let h = 0; - for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) | 0; - for (let i = 0; i < len; i++) { - h = (h * 1103515245 + 12345) | 0; - out.push(0.1 + (Math.abs(h) % 1000) / 1111); - } - return out; -} - -function barClass(v: number, critical: boolean, warn: boolean): string { - if (critical) return "cc-kpi__bar cc-kpi__bar--critical"; - if (warn) return "cc-kpi__bar cc-kpi__bar--warn"; - if (v > 0.55) return "cc-kpi__bar cc-kpi__bar--lit"; - if (v > 0.3) return "cc-kpi__bar cc-kpi__bar--healthy"; - return "cc-kpi__bar"; -} - -// ── Corner Crosshairs ────────────────────────────────────────────────────── - -function Corners() { - return ( - <> - - - - - - ); -} - -// ── Top Status Bar ───────────────────────────────────────────────────────── - -function TopBar({ brands }: { brands: BrandHealth[] }) { - const [clock, setClock] = useState(formatClock()); - useEffect(() => { - const iv = setInterval(() => setClock(formatClock()), 1000); - return () => clearInterval(iv); - }, []); - - // Build a long-enough ticker sequence (duplicated for seamless loop) - const tickerItems = useMemo(() => { - const list = brands.length > 0 - ? brands.map(b => ({ name: b.brand_name, slug: b.brand_slug, plan: b.plan_tier.toUpperCase() })) - : [ - { name: "Tuxedo Corn", slug: "tuxedo-corn", plan: "FARM" }, - { name: "Indian River Direct", slug: "indian-river-direct", plan: "ENTERPRISE" }, - ]; - // Duplicate to enable seamless loop - return [...list, ...list, ...list, ...list]; - }, [brands]); - - return ( -
-
- RC - route.commerce - / - - scope PLATFORM - -
- -
-
- {tickerItems.map((b, i) => ( - - {b.name} - · {b.slug} · {b.plan} - - ))} -
-
- -
- - LIVE - {clock} - UTC - · - {formatToday()} -
-
- ); -} - -// ── KPI Card ────────────────────────────────────────────────────────────── - -function KPICard({ - serial, label, value, unit, sub, sparkSeed, status, delay, -}: { - serial: string; - label: string; - value: string | number; - unit?: string; - sub?: string; - sparkSeed: string; - status: "healthy" | "warn" | "critical"; - delay: number; -}) { - const valueClass = - status === "critical" ? "cc-kpi__value cc-kpi__value--critical" - : status === "warn" ? "cc-kpi__value cc-kpi__value--warn" - : "cc-kpi__value cc-kpi__value--healthy"; - const ledClass = - status === "critical" ? "cc-led cc-led--critical" - : status === "warn" ? "cc-led cc-led--warn" - : "cc-led cc-led--healthy"; - const bars = spark(sparkSeed); - - return ( -
- -
- {label} - -
-
- {typeof value === "number" ? value.toLocaleString() : value} - {unit && {unit}} -
- {sub &&
{sub}
} -
- {bars.map((v, i) => ( - - ))} -
- - {serial} - -
- ); -} - -// ── AI Briefing ──────────────────────────────────────────────────────────── - -function AIBriefing({ metrics, brandHealth }: { - metrics: PlatformMetrics | null; - brandHealth: BrandHealth[]; -}) { - const items = useMemo(() => { - if (!metrics) return [] as { idx: string; text: React.ReactNode }[]; - const out: { idx: string; text: React.ReactNode }[] = []; - if (metrics.orders_today > 0) { - out.push({ idx: "01", text: <>{metrics.orders_today} order{metrics.orders_today !== 1 ? "s" : ""} placed across all brands today. }); - } - if (Number(metrics.revenue_today) > 0) { - out.push({ idx: "02", text: <>{formatCurrencyFull(Number(metrics.revenue_today))} in revenue processed · target pacing on track. }); - } - if (metrics.active_routes > 0) { - out.push({ idx: "03", text: <>{metrics.active_routes} active route{metrics.active_routes !== 1 ? "s" : ""} running · all on schedule. }); - } - if (metrics.pending_orders_today > 0) { - out.push({ idx: "04", text: <>{metrics.pending_orders_today} order{metrics.pending_orders_today !== 1 ? "s" : ""} pending confirmation · review queue. }); - } - if (metrics.failed_orders_today > 0) { - out.push({ idx: "05", text: <>{metrics.failed_orders_today} failed · payment retry window open · triage required. }); - } - const crit = brandHealth.filter(b => b.health_status === "critical").length; - if (crit > 0) { - out.push({ idx: "06", text: <>{crit} brand{crit !== 1 ? "s" : ""} in critical state · investigate pain log entries. }); - } - const warn = brandHealth.filter(b => b.health_status === "warning").length; - if (warn > 0 && crit === 0) { - out.push({ idx: "07", text: <>{warn} brand{warn !== 1 ? "s" : ""} flagged for attention · monitoring. }); - } - if (out.length === 0) { - out.push({ idx: "00", text: PLATFORM NOMINAL · no active issues · all channels green }); - } - return out; - }, [metrics, brandHealth]); - - return ( -
- -
-
- Platform Briefing - - - GENERATED · {new Date().toLocaleTimeString("en-US", { hour12: false })} - -
-
- {items.map(it => ( -
- {it.idx} - {it.text} -
- ))} -
-
-
-
Sourcelive.rpc
-
Window24h
-
Buildcc-26.6
-
-
- ); -} - -// ── Brand Health Panel ───────────────────────────────────────────────────── - -function BrandPanel({ brand, delay }: { brand: BrandHealth; delay: number }) { - const ledClass = HEALTH_LED[brand.health_status] ?? "cc-led--off"; - const statusClass = HEALTH_CLASS[brand.health_status] ?? ""; - const statusLabel = HEALTH_LABEL[brand.health_status] ?? "UNKNOWN"; - - return ( -
- -
-
-
- - {brand.brand_name} -
-
- /{brand.brand_slug} · {brand.plan_tier} -
-
- - {statusLabel} - -
- -
-
- Orders - {brand.orders_today} -
-
- Revenue - {formatCurrency(Number(brand.revenue_today))} -
-
- Routes - 0 ? "cc-brand__metric-value--critical" : ""}`}> - {brand.active_stops} - -
-
- - {brand.open_pain_items > 0 && ( -
- - {brand.open_pain_items} open issue{brand.open_pain_items !== 1 ? "s" : ""} on this brand - Founder Queue -
- )} -
- ); -} - -// ── Pain Log Item ───────────────────────────────────────────────────────── - -function PainItem({ - item, idx, onResolve, onSnooze, -}: { - item: PainLogItem; - idx: number; - onResolve: (id: string) => void; - onSnooze: (id: string) => void; -}) { - const ledClass = SEVERITY_LED[item.severity] ?? "cc-led--off"; - const severityClass = `cc-pain--${item.severity}`; - const serial = `P.${String(idx).padStart(3, "0")}`; - - return ( -
- -
- - - {item.severity} - - {item.title} -
- - -
-
-
- {serial} - · - {item.brand_name && <>{item.brand_name}·} - {item.category} - · - {item.created_at ? timeAgo(item.created_at) : ""} -
-
- ); -} - -// ── Activity Feed Row ────────────────────────────────────────────────────── - -function ActivityRow({ event, idx }: { event: ActivityEvent; idx: number }) { - const codeClass = FEED_CODE_CLASS[event.event_type] ?? "cc-feed__code--default"; - const code = event.event_type.replace(/_/g, " ").toUpperCase(); - - return ( -
- - {event.created_at ? formatTime(event.created_at) : "—"} - - {code} - - {event.brand_name && {event.brand_name}} - {event.entity_type ?? "event"} - {event.entity_id && ( - #{event.entity_id.slice(0, 8)} - )} - - {event.created_at ? timeAgo(event.created_at) : ""} -
- ); -} - -// ── Main ────────────────────────────────────────────────────────────────── - -export default function CommandCenterDashboard() { - const [metrics, setMetrics] = useState(null); - const [activity, setActivity] = useState([]); - const [brandHealth, setBrandHealth] = useState([]); - const [painLog, setPainLog] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [resolving, setResolving] = useState>(new Set()); - const [showAddForm, setShowAddForm] = useState(false); - const [tvMode, setTvMode] = useState(false); - const [addForm, setAddForm] = useState({ severity: "medium", category: "operations", title: "", description: "" }); - const [snoozed, setSnoozed] = useState>(new Set()); - - const fetchAll = useCallback(async () => { - try { - const [m, a, b, p] = await Promise.all([ - getPlatformMetrics(), - getPlatformActivityFeed(), - getBrandHealthSnapshot(), - getPainLog(), - ]); - setMetrics(m); - setActivity(a); - setBrandHealth(b); - setPainLog(p); - setError(null); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load"); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchAll(); - const iv = setInterval(fetchAll, REFRESH_INTERVAL); - return () => clearInterval(iv); - }, [fetchAll]); - - const handleResolve = async (id: string) => { - if (resolving.has(id)) return; - setResolving(prev => new Set([...prev, id])); - try { - const r = await resolvePainLogItem(id); - if (r.success) setPainLog(prev => prev.filter(i => i.id !== id)); - } finally { - setResolving(prev => { const n = new Set(prev); n.delete(id); return n; }); - } - }; - - const handleSnooze = (id: string) => { - setSnoozed(prev => new Set([...prev, id])); - setTimeout(() => { - setSnoozed(prev => { const n = new Set(prev); n.delete(id); return n; }); - }, 3600000); - }; - - const handleAdd = async () => { - if (!addForm.title.trim()) return; - const r = await createPainLogItem({ - severity: addForm.severity as "low" | "medium" | "high" | "critical", - category: addForm.category, - title: addForm.title, - description: addForm.description || null, - }); - if (r.success) { - setAddForm({ severity: "medium", category: "operations", title: "", description: "" }); - setShowAddForm(false); - fetchAll(); - } - }; - - const openItems = painLog.filter(p => p.status === "open" && !snoozed.has(p.id)); - - // ── Loading state ───────────────────────────────────────────────────── - if (loading) { - return ( -
- -
-
-
-
{"// "}00 · BOOT SEQUENCE
-

Command Center

-
-
-
- - Initializing platform channels - {"// "}awaiting telemetry -
-
-
- ); - } - - // ── Error state ("Signal Lost") ──────────────────────────────────────── - if (error) { - return ( -
- -
-
- -
- SIGNAL LOST · COMMS OFFLINE -
-

- Platform Disconnected -

-

- The Command Center cannot reach the platform RPCs right now. - This is usually a transient database or auth issue — the data is safe, the connection isn{`'`}t. -

-
{error}
-
- - Return to admin -
-
- {"// "}SUGGESTED · check DATABASE_URL · re-run migrations - CODE: RPC_UNREACHABLE -
-
-
-
- ); - } - - // ── Main state ──────────────────────────────────────────────────────── - return ( -
- - -
- {/* ── Masthead ───────────────────────────────────────────────────── */} -
-
-
{"// "}01 · PLATFORM OPERATIONS
-

Command Center

-

- Real-time platform telemetry · {brandHealth.length} active brand{brandHealth.length !== 1 ? "s" : ""} · - last sync just now · refresh every 30s -

-
-
- - -
-
- - {/* ── KPI Row ────────────────────────────────────────────────────── */} -
-
- {"// "}02 - KPI OVERVIEW - - 06 -
-
- b.health_status === "healthy").length} healthy`} - sparkSeed="brands" status="healthy" - /> - 0 ? "critical" : "healthy"} - /> - - 5 ? "warn" : "healthy"} - /> - 0 ? "critical" : "healthy"} - /> - 5 ? "warn" : "healthy"} - /> -
-
- - {/* ── AI Briefing ─────────────────────────────────────────────────── */} -
-
- {"// "}03 - DAILY BRIEFING - - AUTO -
- -
- - {/* ── Two-column body ────────────────────────────────────────────── */} -
- {/* LEFT: Brand Health + Activity */} -
-
-
- {"// "}04 - BRAND HEALTH - - {brandHealth.length} -
- {brandHealth.length === 0 ? ( -
-
{"// "}NO ACTIVE BRANDS
-
No brands registered on this platform.
-
- ) : ( -
- {brandHealth.map((b, i) => ( - - ))} -
- )} -
- -
-
- {"// "}05 - LIVE ACTIVITY - - {activity.length} -
-
- -
- {activity.length === 0 ? ( -
-
{"// "}NO EVENTS
-
Waiting for telemetry.
-
- ) : ( - activity.slice(0, 30).map((e, i) => ( - - )) - )} -
-
-
-
- - {/* RIGHT: Founder Queue + Quick Access */} -
-
-
- {"// "}06 - FOUNDER QUEUE - - {openItems.length} - -
- - {showAddForm && ( -
-
NEW PAIN LOG ENTRY
-
- - setAddForm(f => ({ ...f, category: e.target.value }))} - placeholder="category (e.g. deployment)" - className="cc-field" - /> -
- setAddForm(f => ({ ...f, title: e.target.value }))} - placeholder="issue title..." - className="cc-field" - /> -