From 4f22da65d823f6f26681302ab47f5afc44b999f7 Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 17 Jun 2026 09:28:44 -0600 Subject: [PATCH] redesign command center as operations schematic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aesthetic shift from generic dark glassmorphism to an industrial mission-control look: phosphor amber on near-black, Major Mono Display for the wordmark, JetBrains Mono for all labels/numbers/codes, Inter Tight for body copy. - Top status bar with live clock, pulsing LIVE LED, and scrolling brand ticker (plan tiers + slugs) - Serial-numbered sections (// 01-07) with CAD-style corner crosshairs and scale-rule dividers - 6 KPIs in a flush 1px-separated grid with tabular numerals, sparklines, and per-cell M.0N serials - AI briefing as a numbered readout with model metadata sidebar - Brand health panels with severity LEDs, vertical dividers between metrics, and open-pain indicators - Activity feed as terminal-style rows with color-coded event codes - Pain log items with severity LEDs and P.NNN serials - Quick Access links with L.NN prefixes and slide-on-hover - Loading state replaced with terminal-style blinking bar + 'BOOT SEQUENCE' eyebrow - 'Connection Lost' replaced with 'SIGNAL LOST · Platform Disconnected' panel that surfaces the actual error, reconnect action, and a diagnostic hint footer - prefers-reduced-motion respected across ticker, LED pulse, and boot-reveal animations - TV Mode scales up the title, KPIs, and briefing for wall display --- .../admin/command-center/command-center.css | 1143 +++++++++++++++++ src/app/admin/command-center/page.tsx | 44 +- .../admin/CommandCenterDashboard.tsx | 1057 ++++++++------- 3 files changed, 1804 insertions(+), 440 deletions(-) create mode 100644 src/app/admin/command-center/command-center.css diff --git a/src/app/admin/command-center/command-center.css b/src/app/admin/command-center/command-center.css new file mode 100644 index 0000000..da6fe5f --- /dev/null +++ b/src/app/admin/command-center/command-center.css @@ -0,0 +1,1143 @@ +/* ============================================================================ + * 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 index 1e1119b..fd70aaa 100644 --- a/src/app/admin/command-center/page.tsx +++ b/src/app/admin/command-center/page.tsx @@ -2,17 +2,49 @@ 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 ( -
-
- -
-
+
+ +
); -} \ No newline at end of file +} diff --git a/src/components/admin/CommandCenterDashboard.tsx b/src/components/admin/CommandCenterDashboard.tsx index c7bf715..5bfad15 100644 --- a/src/components/admin/CommandCenterDashboard.tsx +++ b/src/components/admin/CommandCenterDashboard.tsx @@ -1,7 +1,7 @@ "use client"; /* eslint-disable react-hooks/set-state-in-effect */ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import { getPlatformMetrics, getPlatformActivityFeed, @@ -15,34 +15,52 @@ import { type PainLogItem, } from "@/actions/platform/command-center"; -// ── Constants ───────────────────────────────────────────────────────────────── +// ── Constants ────────────────────────────────────────────────────────────── const REFRESH_INTERVAL = 30000; -const EVENT_COLORS: Record = { - order_placed: { bg: "bg-violet-500/20", text: "text-violet-300", dot: "bg-violet-400" }, - order_completed: { bg: "bg-emerald-500/20", text: "text-emerald-300", dot: "bg-emerald-400" }, - pickup_completed: { bg: "bg-blue-900/300/20", text: "text-blue-300", dot: "bg-blue-400" }, - payment_failed: { bg: "bg-red-900/300/20", text: "text-red-300", dot: "bg-red-400" }, - stop_created: { bg: "bg-amber-900/300/20", text: "text-amber-300", dot: "bg-amber-400" }, - route_updated: { bg: "bg-cyan-500/20", text: "text-cyan-300", dot: "bg-cyan-400" }, - default: { bg: "bg-zinc-900/10", text: "text-white/60", dot: "bg-zinc-900/40" }, +const SEVERITY_LED: Record = { + critical: "cc-led--critical", + high: "cc-led--warn", + medium: "cc-led cc-led--info", + low: "cc-led--off", }; -const SEVERITY_CONFIG: Record = { - critical: { bg: "bg-red-900/300/15", text: "text-red-300", border: "border-red-500/30" }, - high: { bg: "bg-orange-500/15",text: "text-orange-300", border: "border-orange-500/30" }, - medium: { bg: "bg-amber-900/300/15",text: "text-amber-300", border: "border-amber-500/30" }, - low: { bg: "bg-zinc-900/8", text: "text-white/60", border: "border-white/10" }, +const HEALTH_LED: Record = { + healthy: "cc-led--healthy", + warning: "cc-led--warn", + critical: "cc-led--critical", }; -const HEALTH_CONFIG: Record = { - healthy: { label: "Operational", bg: "bg-emerald-500/15", text: "text-emerald-300", border: "border-emerald-500/25", dot: "bg-emerald-400" }, - warning: { label: "Attention", bg: "bg-amber-900/300/15", text: "text-amber-300", border: "border-amber-500/25", dot: "bg-amber-400" }, - critical: { label: "Critical", bg: "bg-red-900/300/15", text: "text-red-300", border: "border-red-500/25", dot: "bg-red-400" }, +const HEALTH_CLASS: Record = { + healthy: "cc-brand--healthy", + warning: "cc-brand--warning", + critical: "cc-brand--critical", }; -// ── Helpers ─────────────────────────────────────────────────────────────────── +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", { @@ -60,228 +78,363 @@ function formatCurrencyFull(n: number): string { function timeAgo(dateStr: string): string { const diff = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000); - if (diff < 60) return `${diff}s`; - if (diff < 3600) return `${Math.floor(diff / 60)}m`; - if (diff < 86400)return `${Math.floor(diff / 3600)}h`; - return `${Math.floor(diff / 86400)}d`; + 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`; } -// ── Metric Card ───────────────────────────────────────────────────────────────── +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 MetricCard({ - label, - value, - sub, - prefix, - suffix, - status, - delay = 0, - tv = false, +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; - prefix?: string; - suffix?: string; - status?: "healthy" | "warning" | "critical"; - delay?: number; - tv?: boolean; + sparkSeed: string; + status: "healthy" | "warn" | "critical"; + delay: number; }) { - const valueColor = { - healthy: "text-white", - warning: "text-amber-300", - critical: "text-red-300", - }[status ?? "healthy"]!; + 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}

-
- {prefix && {prefix}} - - {typeof value === "number" ? value.toLocaleString() : value} - - {suffix && {suffix}} +
+ +
+ {label} +
- {sub &&

{sub}

} -
- ); -} - -// ── Brand Health Card ───────────────────────────────────────────────────────── - -function BrandCard({ brand, tv = false }: { brand: BrandHealth; tv?: boolean }) { - const c = HEALTH_CONFIG[brand.health_status as keyof typeof HEALTH_CONFIG] ?? HEALTH_CONFIG.warning; - - return ( -
-
-
-

{brand.brand_name}

-

{brand.brand_slug}

-
-
- - {c.label} - -
-
+
+ {typeof value === "number" ? value.toLocaleString() : value} + {unit && {unit}}
- -
- {[ - { label: "Orders", value: brand.orders_today }, - { label: "Revenue", value: formatCurrency(Number(brand.revenue_today)) }, - { label: "Routes", value: brand.active_stops }, - ].map(({ label, value }) => ( -
-

{label}

-

{value}

-
+ {sub &&
{sub}
} +
+ {bars.map((v, i) => ( + ))}
- - {brand.open_pain_items > 0 && ( -
- - - {brand.open_pain_items} open issue{brand.open_pain_items !== 1 ? "s" : ""} - -
- )} + + {serial} +
); } -// ── Activity Item ────────────────────────────────────────────────────────────── - -function ActivityItem({ event }: { event: ActivityEvent }) { - const c = EVENT_COLORS[event.event_type] ?? EVENT_COLORS.default; - const label = event.event_type.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase()); - - return ( -
-
- -
-
-
- - {label} - - {event.brand_name && ( - {event.brand_name} - )} -
-
-

- {event.entity_type && {event.entity_type}} - {event.entity_id && #{event.entity_id.slice(0, 6)}} -

- - {event.created_at ? timeAgo(event.created_at) : ""} - -
-
-
- ); -} - -// ── Pain Item ───────────────────────────────────────────────────────────────── - -function PainItem({ - item, - onResolve, - onSnooze, -}: { - item: PainLogItem; - onResolve: (id: string) => void; - onSnooze: (id: string) => void; -}) { - const c = SEVERITY_CONFIG[item.severity] ?? SEVERITY_CONFIG.low; - - return ( -
-
-
- - {item.severity} - - {item.title} -
-
- - -
-
-
- {item.brand_name && {item.brand_name}} - · - {item.category} - · - {item.created_at ? timeAgo(item.created_at) + " ago" : ""} -
-
- ); -} - -// ── AI Briefing ──────────────────────────────────────────────────────────────── +// ── AI Briefing ──────────────────────────────────────────────────────────── function AIBriefing({ metrics, brandHealth }: { metrics: PlatformMetrics | null; brandHealth: BrandHealth[]; }) { - const lines: string[] = []; - if (!metrics) return
; - - if (metrics.orders_today > 0) { - lines.push(`${metrics.orders_today} order${metrics.orders_today !== 1 ? "s" : ""} placed today.`); - } - if (Number(metrics.revenue_today) > 0) { - lines.push(`${formatCurrencyFull(Number(metrics.revenue_today))} in revenue processed.`); - } - if (metrics.failed_orders_today > 0) { - lines.push(`${metrics.failed_orders_today} failed order${metrics.failed_orders_today !== 1 ? "s" : ""} need attention.`); - } - if (metrics.pending_orders_today > 0) { - lines.push(`${metrics.pending_orders_today} order${metrics.pending_orders_today !== 1 ? "s" : ""} pending confirmation.`); - } - const crit = brandHealth.filter(b => b.health_status === "critical").length; - if (crit > 0) lines.push(`${crit} brand${crit !== 1 ? "s" : ""} in critical state.`); - const warn = brandHealth.filter(b => b.health_status === "warning").length; - if (warn > 0 && crit === 0) lines.push(`${warn} brand${warn !== 1 ? "s" : ""} need attention.`); - - if (lines.length === 0) { - lines.push("Platform is running smoothly — no active issues."); - } + 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 ( -
- {lines.map((line, i) => ( -

- {line} -

- ))} +
+ +
+
+ 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 ───────────────────────────────────────────────────────────────────── +// ── Main ────────────────────────────────────────────────────────────────── export default function CommandCenterDashboard() { const [metrics, setMetrics] = useState(null); @@ -296,8 +449,6 @@ export default function CommandCenterDashboard() { const [addForm, setAddForm] = useState({ severity: "medium", category: "operations", title: "", description: "" }); const [snoozed, setSnoozed] = useState>(new Set()); - const tv = tvMode; - const fetchAll = useCallback(async () => { try { const [m, a, b, p] = await Promise.all([ @@ -316,7 +467,6 @@ export default function CommandCenterDashboard() { } finally { setLoading(false); } - }, []); useEffect(() => { @@ -326,6 +476,7 @@ export default function CommandCenterDashboard() { }, [fetchAll]); const handleResolve = async (id: string) => { + if (resolving.has(id)) return; setResolving(prev => new Set([...prev, id])); try { const r = await resolvePainLogItem(id); @@ -359,264 +510,302 @@ export default function CommandCenterDashboard() { const openItems = painLog.filter(p => p.status === "open" && !snoozed.has(p.id)); + // ── Loading state ───────────────────────────────────────────────────── if (loading) { return ( -
-
-
-

Initializing

+
+ +
+
+
+
{"// "}00 · BOOT SEQUENCE
+

Command Center

+
+
+
+ + Initializing platform channels + {"// "}awaiting telemetry +
); } + // ── Error state ("Signal Lost") ──────────────────────────────────────── if (error) { return ( -
-
-
-

Connection Lost

-

{error}

- +
+ +
+
+ +
+ 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 ( -
+
+ - {/* ── Header ─────────────────────────────────────────────────────────── */} -
-
-

- Command Center -

-

- {new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric" })} - {" · "} - LIVE - -

+
+ {/* ── 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 ──────────────────────────────────────────────────────────── */} -
- - - - 5 ? "warning" : "healthy"} /> - 0 ? "critical" : "healthy"} /> - 5 ? "warning" : "healthy"} /> -
+ {/* ── 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 (full width) ──────────────────────────────────────── */} -
-
-
-
- 🤖 -
-
-

Daily Briefing

-

Platform intelligence summary

-
+ {/* ── AI Briefing ─────────────────────────────────────────────────── */} +
+
+ {"// "}03 + DAILY BRIEFING + + AUTO
-
-
+ - {/* ── 2-Column Body ───────────────────────────────────────────────────── */} -
- - {/* ── LEFT: Brand Health + Activity ───────────────────────────── */} -
- {/* Brand Health */} + {/* ── Two-column body ────────────────────────────────────────────── */} +
+ {/* LEFT: Brand Health + Activity */}
-
-

- Brand Health -

- - {brandHealth.length} - -
-
+
+
+ {"// "}04 + BRAND HEALTH + + {brandHealth.length} +
{brandHealth.length === 0 ? ( -
-

No active brands

+
+
{"// "}NO ACTIVE BRANDS
+
No brands registered on this platform.
) : ( - brandHealth.map((b) => ( - - )) +
+ {brandHealth.map((b, i) => ( + + ))} +
)} -
+
+ +
+
+ {"// "}05 + LIVE ACTIVITY + + {activity.length} +
+
+ +
+ {activity.length === 0 ? ( +
+
{"// "}NO EVENTS
+
Waiting for telemetry.
+
+ ) : ( + activity.slice(0, 30).map((e, i) => ( + + )) + )} +
+
+
- {/* Activity Feed */} + {/* RIGHT: Founder Queue + Quick Access */}
-
-

- Live Activity -

- - {activity.length} - -
-
-
- {activity.length === 0 ? ( -
-

No recent events

+
+
+ {"// "}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" + /> +