Remove command-center page and dependencies
Deploy to route.crispygoat.com / deploy (push) Successful in 4m20s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m20s
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.
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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<string, string> = {
|
||||
critical: "cc-led--critical",
|
||||
high: "cc-led--warn",
|
||||
medium: "cc-led cc-led--info",
|
||||
low: "cc-led--off",
|
||||
};
|
||||
|
||||
const HEALTH_LED: Record<string, string> = {
|
||||
healthy: "cc-led--healthy",
|
||||
warning: "cc-led--warn",
|
||||
critical: "cc-led--critical",
|
||||
};
|
||||
|
||||
const HEALTH_CLASS: Record<string, string> = {
|
||||
healthy: "cc-brand--healthy",
|
||||
warning: "cc-brand--warning",
|
||||
critical: "cc-brand--critical",
|
||||
};
|
||||
|
||||
const HEALTH_LABEL: Record<string, string> = {
|
||||
healthy: "OPERATIONAL",
|
||||
warning: "ATTENTION",
|
||||
critical: "CRITICAL",
|
||||
};
|
||||
|
||||
const FEED_CODE_CLASS: Record<string, string> = {
|
||||
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 (
|
||||
<>
|
||||
<span className="cc-corner cc-corner--tl" />
|
||||
<span className="cc-corner cc-corner--tr" />
|
||||
<span className="cc-corner cc-corner--bl" />
|
||||
<span className="cc-corner cc-corner--br" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Top Status Bar ─────────────────────────────────────────────────────────
|
||||
|
||||
function TopBar({ brands }: { brands: BrandHealth[] }) {
|
||||
const [clock, setClock] = useState<string>(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 (
|
||||
<header className="cc-topbar" role="banner">
|
||||
<div className="cc-topbar__brand">
|
||||
<span className="cc-topbar__brand-mark">RC</span>
|
||||
<span>route.commerce</span>
|
||||
<span className="cc-topbar__sep">/</span>
|
||||
<span className="cc-topbar__scope">
|
||||
scope <b>PLATFORM</b>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="cc-ticker" aria-label="Live brand ticker">
|
||||
<div className="cc-ticker__track">
|
||||
{tickerItems.map((b, i) => (
|
||||
<span key={i} className="cc-ticker__chip">
|
||||
<b>{b.name}</b>
|
||||
<span>· {b.slug} · {b.plan}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="cc-topbar__meta">
|
||||
<span className="cc-led cc-led--live" />
|
||||
<span style={{ color: "var(--cc-amber)" }}>LIVE</span>
|
||||
<span className="cc-topbar__clock">{clock}</span>
|
||||
<span style={{ color: "var(--cc-faint)" }}>UTC</span>
|
||||
<span className="cc-topbar__sep">·</span>
|
||||
<span>{formatToday()}</span>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<div className="cc-kpi cc-rise" data-stagger={delay}>
|
||||
<Corners />
|
||||
<div className="cc-kpi__head">
|
||||
<span className="cc-kpi__label">{label}</span>
|
||||
<span className={ledClass} />
|
||||
</div>
|
||||
<div className={valueClass}>
|
||||
{typeof value === "number" ? value.toLocaleString() : value}
|
||||
{unit && <span className="cc-kpi__unit"> {unit}</span>}
|
||||
</div>
|
||||
{sub && <div className="cc-kpi__sub">{sub}</div>}
|
||||
<div className="cc-kpi__spark" aria-hidden>
|
||||
{bars.map((v, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={barClass(v, status === "critical", status === "warn")}
|
||||
style={{ height: `${Math.max(2, v * 22)}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="cc-kpi__serial" style={{ position: "absolute", right: 12, bottom: 8 }}>
|
||||
{serial}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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: <><b>{metrics.orders_today}</b> order{metrics.orders_today !== 1 ? "s" : ""} placed across all brands today.</> });
|
||||
}
|
||||
if (Number(metrics.revenue_today) > 0) {
|
||||
out.push({ idx: "02", text: <><b>{formatCurrencyFull(Number(metrics.revenue_today))}</b> in revenue processed · target pacing on track.</> });
|
||||
}
|
||||
if (metrics.active_routes > 0) {
|
||||
out.push({ idx: "03", text: <><b>{metrics.active_routes}</b> active route{metrics.active_routes !== 1 ? "s" : ""} running · all on schedule.</> });
|
||||
}
|
||||
if (metrics.pending_orders_today > 0) {
|
||||
out.push({ idx: "04", text: <><b>{metrics.pending_orders_today}</b> order{metrics.pending_orders_today !== 1 ? "s" : ""} pending confirmation · review queue.</> });
|
||||
}
|
||||
if (metrics.failed_orders_today > 0) {
|
||||
out.push({ idx: "05", text: <><em>{metrics.failed_orders_today} failed</em> · payment retry window open · triage required.</> });
|
||||
}
|
||||
const crit = brandHealth.filter(b => b.health_status === "critical").length;
|
||||
if (crit > 0) {
|
||||
out.push({ idx: "06", text: <><em>{crit} brand{crit !== 1 ? "s" : ""}</em> 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: <><b>{warn}</b> brand{warn !== 1 ? "s" : ""} flagged for attention · monitoring.</> });
|
||||
}
|
||||
if (out.length === 0) {
|
||||
out.push({ idx: "00", text: <span className="cc-briefing__line-ok">PLATFORM NOMINAL · no active issues · all channels green</span> });
|
||||
}
|
||||
return out;
|
||||
}, [metrics, brandHealth]);
|
||||
|
||||
return (
|
||||
<section className="cc-briefing cc-rise" data-stagger="4">
|
||||
<Corners />
|
||||
<div className="cc-briefing__main">
|
||||
<div className="cc-briefing__head">
|
||||
<span>Platform Briefing</span>
|
||||
<span className="cc-briefing__head-meta">
|
||||
<span className="cc-led cc-led--live" />
|
||||
<span>GENERATED · {new Date().toLocaleTimeString("en-US", { hour12: false })}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="cc-briefing__lines">
|
||||
{items.map(it => (
|
||||
<div key={it.idx} className="cc-briefing__line">
|
||||
<span className="cc-briefing__line-idx">{it.idx}</span>
|
||||
<span>{it.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cc-briefing__model" aria-label="Model metadata">
|
||||
<div className="cc-briefing__model-row"><span>Source</span><b>live.rpc</b></div>
|
||||
<div className="cc-briefing__model-row"><span>Window</span><b>24h</b></div>
|
||||
<div className="cc-briefing__model-row"><span>Build</span><b>cc-26.6</b></div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<article className={`cc-brand ${statusClass} cc-rise`} data-stagger={delay}>
|
||||
<Corners />
|
||||
<div className="cc-brand__head">
|
||||
<div>
|
||||
<div className="cc-brand__name">
|
||||
<span className={`cc-led ${ledClass}`} />
|
||||
{brand.brand_name}
|
||||
</div>
|
||||
<div className="cc-brand__slug">
|
||||
<b>/{brand.brand_slug}</b> · {brand.plan_tier}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`cc-brand__status cc-brand__status--${brand.health_status}`}>
|
||||
{statusLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="cc-brand__metrics">
|
||||
<div className="cc-brand__metric">
|
||||
<span className="cc-brand__metric-label">Orders</span>
|
||||
<span className="cc-brand__metric-value">{brand.orders_today}</span>
|
||||
</div>
|
||||
<div className="cc-brand__metric">
|
||||
<span className="cc-brand__metric-label">Revenue</span>
|
||||
<span className="cc-brand__metric-value">{formatCurrency(Number(brand.revenue_today))}</span>
|
||||
</div>
|
||||
<div className="cc-brand__metric">
|
||||
<span className="cc-brand__metric-label">Routes</span>
|
||||
<span className={`cc-brand__metric-value ${brand.failed_orders > 0 ? "cc-brand__metric-value--critical" : ""}`}>
|
||||
{brand.active_stops}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{brand.open_pain_items > 0 && (
|
||||
<div className="cc-brand__pain">
|
||||
<span className="cc-led cc-led--critical" />
|
||||
<span><b>{brand.open_pain_items}</b> open issue{brand.open_pain_items !== 1 ? "s" : ""} on this brand</span>
|
||||
<span>Founder Queue</span>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<article className={`cc-pain ${severityClass} cc-rise`} data-stagger={Math.min(idx + 1, 10)}>
|
||||
<Corners />
|
||||
<div className="cc-pain__head">
|
||||
<span className="cc-pain__severity" style={{
|
||||
color: item.severity === "critical" ? "var(--cc-critical)"
|
||||
: item.severity === "high" ? "var(--cc-warn)"
|
||||
: item.severity === "medium" ? "var(--cc-amber)"
|
||||
: "var(--cc-muted)"
|
||||
}}>
|
||||
<span className={ledClass} />
|
||||
{item.severity}
|
||||
</span>
|
||||
<span className="cc-pain__title">{item.title}</span>
|
||||
<div className="cc-pain__actions">
|
||||
<button onClick={() => onSnooze(item.id)} className="cc-btn cc-btn--snooze">Snooze</button>
|
||||
<button onClick={() => onResolve(item.id)} className="cc-btn cc-btn--resolved">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cc-pain__foot">
|
||||
<span>{serial}</span>
|
||||
<span className="cc-pain__foot-sep">·</span>
|
||||
{item.brand_name && <><span><b>{item.brand_name}</b></span><span className="cc-pain__foot-sep">·</span></>}
|
||||
<span><b>{item.category}</b></span>
|
||||
<span className="cc-pain__foot-sep">·</span>
|
||||
<span>{item.created_at ? timeAgo(item.created_at) : ""}</span>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<div className="cc-feed__row cc-rise" data-stagger={Math.min(idx + 1, 10)}>
|
||||
<span className="cc-feed__time">
|
||||
{event.created_at ? formatTime(event.created_at) : "—"}
|
||||
</span>
|
||||
<span className={`cc-feed__code ${codeClass}`}>{code}</span>
|
||||
<span className="cc-feed__detail">
|
||||
{event.brand_name && <span className="cc-feed__brand">{event.brand_name}</span>}
|
||||
<span className="cc-feed__entity">{event.entity_type ?? "event"}</span>
|
||||
{event.entity_id && (
|
||||
<span className="cc-feed__entity-id">#{event.entity_id.slice(0, 8)}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="cc-feed__ago">{event.created_at ? timeAgo(event.created_at) : ""}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function CommandCenterDashboard() {
|
||||
const [metrics, setMetrics] = useState<PlatformMetrics | null>(null);
|
||||
const [activity, setActivity] = useState<ActivityEvent[]>([]);
|
||||
const [brandHealth, setBrandHealth] = useState<BrandHealth[]>([]);
|
||||
const [painLog, setPainLog] = useState<PainLogItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [resolving, setResolving] = useState<Set<string>>(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<Set<string>>(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 (
|
||||
<div className="cc-schematic" data-tv={tvMode ? "true" : "false"}>
|
||||
<TopBar brands={[]} />
|
||||
<div className="cc-frame">
|
||||
<div className="cc-masthead cc-rise">
|
||||
<div>
|
||||
<div className="cc-masthead__eyebrow">{"// "}00 · BOOT SEQUENCE</div>
|
||||
<h1 className="cc-masthead__title">Command <em>Center</em></h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cc-skeleton cc-rise" data-stagger="1" style={{ marginTop: 32 }}>
|
||||
<span className="cc-skeleton__bar">▌<span>▌</span><span>▌</span></span>
|
||||
<span>Initializing platform channels</span>
|
||||
<span style={{ marginLeft: "auto", color: "var(--cc-faint)" }}>{"// "}awaiting telemetry</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Error state ("Signal Lost") ────────────────────────────────────────
|
||||
if (error) {
|
||||
return (
|
||||
<div className="cc-schematic" data-tv={tvMode ? "true" : "false"}>
|
||||
<TopBar brands={[]} />
|
||||
<div className="cc-signal-lost">
|
||||
<div className="cc-signal-lost__panel cc-rise">
|
||||
<Corners />
|
||||
<div className="cc-signal-lost__eyebrow">
|
||||
<span>SIGNAL LOST · COMMS OFFLINE</span>
|
||||
</div>
|
||||
<h2 className="cc-signal-lost__title">
|
||||
Platform <em>Disconnected</em>
|
||||
</h2>
|
||||
<p className="cc-signal-lost__sub">
|
||||
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.
|
||||
</p>
|
||||
<div className="cc-signal-lost__error">{error}</div>
|
||||
<div className="cc-signal-lost__actions">
|
||||
<button onClick={fetchAll} className="cc-btn cc-btn--primary">
|
||||
<span>↻</span> Reconnect
|
||||
</button>
|
||||
<a href="/admin" className="cc-btn cc-btn--ghost">Return to admin</a>
|
||||
</div>
|
||||
<div className="cc-signal-lost__hint">
|
||||
<span>{"// "}SUGGESTED · check DATABASE_URL · re-run migrations</span>
|
||||
<span>CODE: <b style={{ color: "var(--cc-critical)" }}>RPC_UNREACHABLE</b></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main state ────────────────────────────────────────────────────────
|
||||
return (
|
||||
<div className="cc-schematic" data-tv={tvMode ? "true" : "false"}>
|
||||
<TopBar brands={brandHealth} />
|
||||
|
||||
<div className="cc-frame">
|
||||
{/* ── Masthead ───────────────────────────────────────────────────── */}
|
||||
<div className="cc-masthead cc-rise">
|
||||
<div>
|
||||
<div className="cc-masthead__eyebrow">{"// "}01 · PLATFORM OPERATIONS</div>
|
||||
<h1 className="cc-masthead__title">Command <em>Center</em></h1>
|
||||
<p className="cc-masthead__sub">
|
||||
Real-time platform telemetry · {brandHealth.length} active brand{brandHealth.length !== 1 ? "s" : ""} ·
|
||||
last sync <b>just now</b> · refresh every <b>30s</b>
|
||||
</p>
|
||||
</div>
|
||||
<div className="cc-masthead__actions">
|
||||
<button onClick={fetchAll} className="cc-btn cc-btn--ghost">
|
||||
<span>↻</span> Refresh
|
||||
</button>
|
||||
<button onClick={() => setTvMode(v => !v)} className="cc-btn">
|
||||
{tvMode ? "Exit TV" : "TV Mode"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── KPI Row ────────────────────────────────────────────────────── */}
|
||||
<section className="cc-section">
|
||||
<div className="cc-section__head">
|
||||
<span className="cc-section__serial">{"// "}02</span>
|
||||
<span className="cc-section__title">KPI OVERVIEW</span>
|
||||
<span className="cc-section__rule" />
|
||||
<span className="cc-section__count">06</span>
|
||||
</div>
|
||||
<div className="cc-kpi-grid">
|
||||
<KPICard
|
||||
serial="M.01" label="Active Brands" delay={1}
|
||||
value={metrics?.active_brands ?? 0}
|
||||
sub={`${brandHealth.filter(b => b.health_status === "healthy").length} healthy`}
|
||||
sparkSeed="brands" status="healthy"
|
||||
/>
|
||||
<KPICard
|
||||
serial="M.02" label="Orders Today" delay={2}
|
||||
value={metrics?.orders_today ?? 0}
|
||||
sub={`${metrics?.pending_orders_today ?? 0} pending`}
|
||||
sparkSeed="orders" status={(metrics?.failed_orders_today ?? 0) > 0 ? "critical" : "healthy"}
|
||||
/>
|
||||
<KPICard
|
||||
serial="M.03" label="Revenue Today" delay={3}
|
||||
value={formatCurrency(Number(metrics?.revenue_today ?? 0))}
|
||||
sub="24h window"
|
||||
sparkSeed="revenue" status="healthy"
|
||||
/>
|
||||
<KPICard
|
||||
serial="M.04" label="Active Routes" delay={4}
|
||||
value={metrics?.active_routes ?? 0}
|
||||
sub="In field"
|
||||
sparkSeed="routes" status={(metrics?.pending_orders_today ?? 0) > 5 ? "warn" : "healthy"}
|
||||
/>
|
||||
<KPICard
|
||||
serial="M.05" label="Failed Orders" delay={5}
|
||||
value={metrics?.failed_orders_today ?? 0}
|
||||
sub={metrics?.failed_orders_today ? "Needs attention" : "All clean"}
|
||||
sparkSeed="failed" status={(metrics?.failed_orders_today ?? 0) > 0 ? "critical" : "healthy"}
|
||||
/>
|
||||
<KPICard
|
||||
serial="M.06" label="Pending Orders" delay={6}
|
||||
value={metrics?.pending_orders_today ?? 0}
|
||||
sub="Awaiting confirmation"
|
||||
sparkSeed="pending" status={(metrics?.pending_orders_today ?? 0) > 5 ? "warn" : "healthy"}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── AI Briefing ─────────────────────────────────────────────────── */}
|
||||
<section className="cc-section">
|
||||
<div className="cc-section__head">
|
||||
<span className="cc-section__serial">{"// "}03</span>
|
||||
<span className="cc-section__title">DAILY BRIEFING</span>
|
||||
<span className="cc-section__rule" />
|
||||
<span className="cc-section__count">AUTO</span>
|
||||
</div>
|
||||
<AIBriefing metrics={metrics} brandHealth={brandHealth} />
|
||||
</section>
|
||||
|
||||
{/* ── Two-column body ────────────────────────────────────────────── */}
|
||||
<div className="cc-grid">
|
||||
{/* LEFT: Brand Health + Activity */}
|
||||
<div>
|
||||
<section className="cc-section">
|
||||
<div className="cc-section__head">
|
||||
<span className="cc-section__serial">{"// "}04</span>
|
||||
<span className="cc-section__title">BRAND HEALTH</span>
|
||||
<span className="cc-section__rule" />
|
||||
<span className="cc-section__count">{brandHealth.length}</span>
|
||||
</div>
|
||||
{brandHealth.length === 0 ? (
|
||||
<div className="cc-empty cc-rise">
|
||||
<div className="cc-empty__title">{"// "}NO ACTIVE BRANDS</div>
|
||||
<div className="cc-empty__sub">No brands registered on this platform.</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "grid", gap: 16 }}>
|
||||
{brandHealth.map((b, i) => (
|
||||
<BrandPanel key={b.brand_id} brand={b} delay={i + 1} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="cc-section">
|
||||
<div className="cc-section__head">
|
||||
<span className="cc-section__serial">{"// "}05</span>
|
||||
<span className="cc-section__title">LIVE ACTIVITY</span>
|
||||
<span className="cc-section__rule" />
|
||||
<span className="cc-section__count">{activity.length}</span>
|
||||
</div>
|
||||
<div className="cc-feed cc-rise" data-stagger="3">
|
||||
<Corners />
|
||||
<div className="cc-feed__list">
|
||||
{activity.length === 0 ? (
|
||||
<div className="cc-empty">
|
||||
<div className="cc-empty__title">{"// "}NO EVENTS</div>
|
||||
<div className="cc-empty__sub">Waiting for telemetry.</div>
|
||||
</div>
|
||||
) : (
|
||||
activity.slice(0, 30).map((e, i) => (
|
||||
<ActivityRow key={e.id} event={e} idx={i} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* RIGHT: Founder Queue + Quick Access */}
|
||||
<div>
|
||||
<section className="cc-section">
|
||||
<div className="cc-section__head">
|
||||
<span className="cc-section__serial">{"// "}06</span>
|
||||
<span className="cc-section__title">FOUNDER QUEUE</span>
|
||||
<span className="cc-section__rule" />
|
||||
<span className="cc-section__count">{openItems.length}</span>
|
||||
<button
|
||||
onClick={() => setShowAddForm(v => !v)}
|
||||
className="cc-btn cc-btn--ghost"
|
||||
style={{ marginLeft: 12 }}
|
||||
>
|
||||
{showAddForm ? "Cancel" : "+ Log Issue"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAddForm && (
|
||||
<div className="cc-form cc-rise" data-stagger="1" style={{ marginBottom: 16 }}>
|
||||
<div className="cc-form__head">NEW PAIN LOG ENTRY</div>
|
||||
<div className="cc-form__row">
|
||||
<select
|
||||
value={addForm.severity}
|
||||
onChange={e => setAddForm(f => ({ ...f, severity: e.target.value }))}
|
||||
className="cc-field"
|
||||
>
|
||||
<option value="low">LOW</option>
|
||||
<option value="medium">MEDIUM</option>
|
||||
<option value="high">HIGH</option>
|
||||
<option value="critical">CRITICAL</option>
|
||||
</select>
|
||||
<input
|
||||
value={addForm.category}
|
||||
onChange={e => setAddForm(f => ({ ...f, category: e.target.value }))}
|
||||
placeholder="category (e.g. deployment)"
|
||||
className="cc-field"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
value={addForm.title}
|
||||
onChange={e => setAddForm(f => ({ ...f, title: e.target.value }))}
|
||||
placeholder="issue title..."
|
||||
className="cc-field"
|
||||
/>
|
||||
<textarea
|
||||
value={addForm.description}
|
||||
onChange={e => setAddForm(f => ({ ...f, description: e.target.value }))}
|
||||
placeholder="description (optional)"
|
||||
rows={2}
|
||||
className="cc-field"
|
||||
style={{ resize: "none", fontFamily: "var(--cc-font-mono)" }}
|
||||
/>
|
||||
<div className="cc-form__actions">
|
||||
<button onClick={handleAdd} className="cc-btn cc-btn--primary">Log Issue</button>
|
||||
<button onClick={() => setShowAddForm(false)} className="cc-btn cc-btn--ghost">Discard</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openItems.length === 0 ? (
|
||||
<div className="cc-empty cc-rise">
|
||||
<div className="cc-empty__title">{"// "}QUEUE CLEAR</div>
|
||||
<div className="cc-empty__sub">No open founder pain items. Add one to track it here.</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "grid", gap: 12 }}>
|
||||
{openItems.map((item, i) => (
|
||||
<PainItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
idx={i + 1}
|
||||
onResolve={handleResolve}
|
||||
onSnooze={handleSnooze}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="cc-section">
|
||||
<div className="cc-section__head">
|
||||
<span className="cc-section__serial">{"// "}07</span>
|
||||
<span className="cc-section__title">QUICK ACCESS</span>
|
||||
<span className="cc-section__rule" />
|
||||
<span className="cc-section__count">{QUICK_ACCESS.length}</span>
|
||||
</div>
|
||||
<div className="cc-links cc-rise" data-stagger="2">
|
||||
<Corners />
|
||||
{QUICK_ACCESS.map((link, i) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="cc-link"
|
||||
>
|
||||
<span className="cc-link__serial">L.{String(i + 1).padStart(2, "0")}</span>
|
||||
<span className="cc-link__label">{link.label}</span>
|
||||
<span className="cc-link__arrow">→</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -45,16 +45,6 @@ export const PALETTE_ENTRIES: PaletteEntry[] = [
|
||||
iconName: "LayoutDashboard",
|
||||
keywords: ["home", "overview"],
|
||||
},
|
||||
{
|
||||
id: "page-command-center",
|
||||
type: "page",
|
||||
label: "Command Center",
|
||||
href: "/admin/command-center",
|
||||
category: "Workspace",
|
||||
iconName: "Command",
|
||||
keywords: ["platform", "admin", "ops", "triage"],
|
||||
},
|
||||
|
||||
// ── Operations ─────────────────────────────────────────────────────
|
||||
{
|
||||
id: "page-orders",
|
||||
|
||||
Reference in New Issue
Block a user