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:
@@ -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;
|
||||||
@@ -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) |
|
| Migrations | **135 SQL files** (production schema, RLS, SECURITY DEFINER RPCs, audit logs) |
|
||||||
| Source | **544 files** (362 .tsx + 179 .ts) |
|
| 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) |
|
| 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 |
|
| 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) |
|
| Integrations | Stripe (Connect + subscriptions), Square (POS sync), FedEx (rates + labels), Resend (email), Twilio (SMS), OpenAI (8 AI tools) |
|
||||||
|
|||||||
@@ -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<string, unknown>;
|
|
||||||
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<T>(
|
|
||||||
rpcName: string,
|
|
||||||
kind: "scalar" | "setof" = "scalar",
|
|
||||||
): Promise<T> {
|
|
||||||
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<Record<string, T>>(
|
|
||||||
`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<PlatformMetrics> {
|
|
||||||
return platformRPC<PlatformMetrics>("get_platform_command_center_metrics", "scalar");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPlatformActivityFeed(): Promise<ActivityEvent[]> {
|
|
||||||
return platformRPC<ActivityEvent[]>("get_platform_activity_feed", "setof");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getBrandHealthSnapshot(): Promise<BrandHealth[]> {
|
|
||||||
return platformRPC<BrandHealth[]>("get_brand_health_snapshot", "setof");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPainLog(): Promise<PainLogItem[]> {
|
|
||||||
const adminUser = await getAdminUser();
|
|
||||||
if (!adminUser) throw new Error("Not authenticated");
|
|
||||||
|
|
||||||
const { rows } = await pool.query<PainLogItem>(
|
|
||||||
`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) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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 <AdminAccessDenied />;
|
|
||||||
if (adminUser.role !== "platform_admin") return <AdminAccessDenied message="Platform admin access required." />;
|
|
||||||
if (adminUser.must_change_password) redirect("/change-password");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${majorMono.variable} ${jetbrainsMono.variable} ${interTight.variable} cc-schematic`}
|
|
||||||
>
|
|
||||||
<CommandCenterDashboard />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -80,9 +80,7 @@ export default function AdminHeader({ userRole, canManageUsers, routeTraceEnable
|
|||||||
? [{ href: "/admin/route-trace", label: "Route Trace" }]
|
? [{ href: "/admin/route-trace", label: "Route Trace" }]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const fullNavLinks = BASE_NAV_LINKS.concat(
|
const fullNavLinks = BASE_NAV_LINKS.concat(moduleLinks);
|
||||||
userRole === "platform_admin" ? [{ href: "/admin/command-center", label: "Command Center" }] : []
|
|
||||||
).concat(moduleLinks);
|
|
||||||
|
|
||||||
const navLinks = isStoreEmployee ? EMPLOYEE_NAV_LINKS : fullNavLinks;
|
const navLinks = isStoreEmployee ? EMPLOYEE_NAV_LINKS : fullNavLinks;
|
||||||
const homeHref = isStoreEmployee ? "/admin/wholesale" : "/admin";
|
const homeHref = isStoreEmployee ? "/admin/wholesale" : "/admin";
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
Ship,
|
Ship,
|
||||||
Mail,
|
Mail,
|
||||||
Store,
|
Store,
|
||||||
Sparkles,
|
|
||||||
Upload,
|
Upload,
|
||||||
BrainCircuit,
|
BrainCircuit,
|
||||||
Clock,
|
Clock,
|
||||||
@@ -74,12 +73,6 @@ const NAV_GROUPS: NavGroup[] = [
|
|||||||
label: "Workspace",
|
label: "Workspace",
|
||||||
items: [
|
items: [
|
||||||
{ href: "/admin", label: "Dashboard", icon: LayoutDashboard },
|
{ 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",
|
iconName: "LayoutDashboard",
|
||||||
keywords: ["home", "overview"],
|
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 ─────────────────────────────────────────────────────
|
// ── Operations ─────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
id: "page-orders",
|
id: "page-orders",
|
||||||
|
|||||||
Reference in New Issue
Block a user