diff --git a/dashboard/app/analytics/page.js b/dashboard/app/analytics/page.js index 36ffeba..7cac13c 100644 --- a/dashboard/app/analytics/page.js +++ b/dashboard/app/analytics/page.js @@ -15,7 +15,9 @@ import { YAxis, } from 'recharts'; import { motion } from 'framer-motion'; -import { BarChart3 } from 'lucide-react'; +import { BarChart3, ChevronUp, ChevronDown } from 'lucide-react'; +import { Skeleton } from '../components/ui/Skeleton'; +import { EmptyState } from '../components/ui/EmptyState'; import NavBar from '../components/NavBar.jsx'; import { useTheme } from '../providers/ThemeProvider.jsx'; import { PAGE_ENTER, STAGGER_CHILDREN, CARD_ITEM } from '../lib/motion.js'; @@ -28,15 +30,15 @@ function KpiCard({ label, value, sub, trend, accentColor = 'var(--accent-cyan)' variants={CARD_ITEM} className="glass-panel glass-edge relative overflow-hidden p-6 group" > -

{label}

+

{label}

{value}

-

{sub}

+

{sub}

{trend !== undefined && ( -
0 ? 'text-[var(--accent-green)]' : 'text-[var(--accent-red)]'}`}> - {trend > 0 ? 'β–²' : 'β–Ό'} +
0 ? 'text-[var(--accent-green)]' : 'text-[var(--accent-red)]'}`}> + {trend > 0 ? : } {Math.abs(trend)}%
)} @@ -57,11 +59,11 @@ function ChartPanel({ title, subtitle, children }) { className="glass-panel p-6 space-y-6" >
-

+

{title}

{subtitle && ( -

{subtitle}

+

{subtitle}

)}
@@ -115,16 +117,16 @@ export default function AnalyticsPage() {
-
+
{[0, 1, 2, 3].map((i) => ( -
+ ))}
-
+
-
-
+ +
@@ -164,19 +166,17 @@ export default function AnalyticsPage() {
-

Operations Intelligence

+

Operations Intelligence

30-Day Performance

{(!data || Object.keys(data).length === 0) ? ( -
-
- -
-

No Intelligence Data

-

- Historical performance metrics are calculated every 24 hours. Check back once your first disruption protocol has been executed. -

+
+
) : ( <> diff --git a/dashboard/app/components/AgentPanel.jsx b/dashboard/app/components/AgentPanel.jsx index 54a5e56..5417ef0 100644 --- a/dashboard/app/components/AgentPanel.jsx +++ b/dashboard/app/components/AgentPanel.jsx @@ -4,14 +4,16 @@ import { useEffect, useRef } from 'react'; import dynamic from 'next/dynamic'; import { AnimatePresence, motion } from 'framer-motion'; +import { PanelSkeleton } from './ui/PanelSkeleton'; + const AgentChatSidebar = dynamic(() => import('./agent/AgentChatSidebar.jsx'), { ssr: false, - loading: () =>
Loading agent reasoning...
, + loading: () => , }); const NewsFeed = dynamic(() => import('./news/NewsFeed.jsx'), { ssr: false, - loading: () =>
Loading news intelligence...
, + loading: () => , }); const TABS = [ diff --git a/dashboard/app/components/NavBar.jsx b/dashboard/app/components/NavBar.jsx index 1e8c2e3..394fbf2 100644 --- a/dashboard/app/components/NavBar.jsx +++ b/dashboard/app/components/NavBar.jsx @@ -84,18 +84,26 @@ export default function NavBar() { : pathname.startsWith(href); const showDivider = idx > 0 && NAV_ITEMS[idx - 1].section !== section; + const sectionLabel = showDivider ? (NAV_ITEMS[idx].section === 'live' ? 'Monitor' : 'Intel') : null; return ( -
+
{showDivider && ( -
+
+
+ {sectionLabel && ( + + {sectionLabel} + + )} +
)} {} : setInternalFilter; + const setF = externalFilter ? () => { } : setInternalFilter; const [t, setT] = useState(null); const [zoomLevel, setZoomLevel] = useState('far'); const [viewerEpoch, setViewerEpoch] = useState(0); @@ -161,7 +161,7 @@ export default function GlobeView({ simulationControlsOpen = false, filter: exte useEffect(() => { if (!cRef.current || vRef.current) return; - let cleanup = () => {}; + let cleanup = () => { }; let initFrameId = null; let cancelled = false; @@ -186,13 +186,13 @@ export default function GlobeView({ simulationControlsOpen = false, filter: exte baseLayer: ionToken ? ImageryLayer.fromWorldImagery({ style: IonWorldImageryStyle.AERIAL_WITH_LABELS }) : new ImageryLayer( - new UrlTemplateImageryProvider({ - url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: ['a', 'b', 'c'], - credit: 'Map data Β© OpenStreetMap contributors', - maximumLevel: 19, - }) - ) + new UrlTemplateImageryProvider({ + url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + credit: 'Map data Β© OpenStreetMap contributors', + maximumLevel: 19, + }) + ) }); if (cancelled) { v.destroy(); @@ -466,41 +466,41 @@ export default function GlobeView({ simulationControlsOpen = false, filter: exte ports.set(route.destKey, { label: route.destLabel, lat: destLat, lon: destLon }); }); - for (const [name, labelEntity] of portLabelEntitiesRef.current) { - if (!ports.has(name)) { - entities.remove(labelEntity); - portLabelEntitiesRef.current.delete(name); + for (const [name, labelEntity] of portLabelEntitiesRef.current) { + if (!ports.has(name)) { + entities.remove(labelEntity); + portLabelEntitiesRef.current.delete(name); + } } - } - for (const [name, port] of ports) { - const existing = portLabelEntitiesRef.current.get(name); - if (existing) { - existing.position = Cartesian3.fromDegrees(port.lon, port.lat); - existing.label.text = port.label; - } else { - const labelEntity = entities.add({ - id: `port-${name}`, - position: Cartesian3.fromDegrees(port.lon, port.lat), - point: { pixelSize: 5, color: Color.fromCssColorString('#e2e8f0') }, - label: { - text: name, - font: 'bold 14px sans-serif', - style: LabelStyle.FILL_AND_OUTLINE, - fillColor: Color.WHITE, - outlineColor: Color.BLACK, - outlineWidth: 2, - pixelOffset: new Cartesian2(0, -14), - translucencyByDistance: new NearFarScalar(1.5e6, 1.0, 5.0e6, 0.0), - scaleByDistance: new NearFarScalar(1.5e6, 1.0, 5.0e6, 0.5), - disableDepthTestDistance: Number.POSITIVE_INFINITY, - }, - properties: { kind: 'port-label', label: name }, - show: new ConstantProperty(false), - }); - portLabelEntitiesRef.current.set(name, labelEntity); + for (const [name, port] of ports) { + const existing = portLabelEntitiesRef.current.get(name); + if (existing) { + existing.position = Cartesian3.fromDegrees(port.lon, port.lat); + existing.label.text = port.label; + } else { + const labelEntity = entities.add({ + id: `port-${name}`, + position: Cartesian3.fromDegrees(port.lon, port.lat), + point: { pixelSize: 5, color: Color.fromCssColorString('#e2e8f0') }, + label: { + text: name, + font: 'bold 14px sans-serif', + style: LabelStyle.FILL_AND_OUTLINE, + fillColor: Color.WHITE, + outlineColor: Color.BLACK, + outlineWidth: 2, + pixelOffset: new Cartesian2(0, -14), + translucencyByDistance: new NearFarScalar(1.5e6, 1.0, 5.0e6, 0.0), + scaleByDistance: new NearFarScalar(1.5e6, 1.0, 5.0e6, 0.5), + disableDepthTestDistance: Number.POSITIVE_INFINITY, + }, + properties: { kind: 'port-label', label: name }, + show: new ConstantProperty(false), + }); + portLabelEntitiesRef.current.set(name, labelEntity); + } } - } viewer.scene.requestRender(); } finally { @@ -829,17 +829,17 @@ export default function GlobeView({ simulationControlsOpen = false, filter: exte showSimulationControls={simulationControlsOpen} />
-

{t?.kind || 'Entity'}

diff --git a/dashboard/app/components/ui/EmptyState.jsx b/dashboard/app/components/ui/EmptyState.jsx new file mode 100644 index 0000000..903ba61 --- /dev/null +++ b/dashboard/app/components/ui/EmptyState.jsx @@ -0,0 +1,27 @@ +export function EmptyState({ + icon, + title, + description, + action +}) { + return ( +
+
+ {icon} +
+
+

+ {title} +

+

+ {description} +

+
+ {action && ( +
+ {action} +
+ )} +
+ ); +} diff --git a/dashboard/app/components/ui/GlobeSkeleton.jsx b/dashboard/app/components/ui/GlobeSkeleton.jsx new file mode 100644 index 0000000..d6e7bf5 --- /dev/null +++ b/dashboard/app/components/ui/GlobeSkeleton.jsx @@ -0,0 +1,26 @@ +import { Skeleton } from './Skeleton'; + +export function GlobeSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+ Initializing Orbital Command +
+
+
+
+
+
+
+
+
+ ); +} diff --git a/dashboard/app/components/ui/PanelSkeleton.jsx b/dashboard/app/components/ui/PanelSkeleton.jsx new file mode 100644 index 0000000..216ab68 --- /dev/null +++ b/dashboard/app/components/ui/PanelSkeleton.jsx @@ -0,0 +1,25 @@ +import { Skeleton } from './Skeleton'; + +export function PanelSkeleton() { + return ( +
+
+ +
+ + +
+
+
+ + + + +
+
+ + +
+
+ ); +} diff --git a/dashboard/app/components/ui/Skeleton.jsx b/dashboard/app/components/ui/Skeleton.jsx new file mode 100644 index 0000000..32c91fb --- /dev/null +++ b/dashboard/app/components/ui/Skeleton.jsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +export function Skeleton({ className, variant = 'line', ...props }) { + return ( +
+ ); +} diff --git a/dashboard/app/demo/page.js b/dashboard/app/demo/page.js index 82781d9..156f711 100644 --- a/dashboard/app/demo/page.js +++ b/dashboard/app/demo/page.js @@ -12,6 +12,24 @@ import { } from 'firebase/firestore'; import { db, isFirebaseConfigured } from '../lib/firebase.js'; import { watchDisruptionSupabase, watchImpactSupabase, watchResolutionSupabase } from '../lib/supabaseWatcher.js'; +import { + Wind, + Anchor, + Construction, + Circle, + Zap, + Radio, + BarChart3, + Bot, + Scale, + CheckCircle2, + FileText, + AlertTriangle, + ChevronRight, + Download, + RotateCcw, + X +} from 'lucide-react'; import NavBar from '../components/NavBar.jsx'; const SCENARIOS = [ @@ -20,7 +38,7 @@ const SCENARIOS = [ label: 'Super Typhoon Mawar', region: 'Western Pacific', severity: 9, - icon: 'πŸŒ€', + icon: Wind, tag: 'CAT 5 STORM', tagColor: 'var(--accent-red)', preview: 'Shanghai β†’ LA corridor at risk. Manila, Kaohsiung, HK vessel advisories active.', @@ -30,7 +48,7 @@ const SCENARIOS = [ label: 'Suez Canal Emergency', region: 'Red Sea / Egypt', severity: 10, - icon: 'βš“', + icon: Anchor, tag: 'CANAL BLOCKED', tagColor: 'var(--accent-amber)', preview: "$12B daily trade halted. 43 vessels held. Lloyd's suspended war-risk coverage.", @@ -40,7 +58,7 @@ const SCENARIOS = [ label: 'Mumbai JNPT Strike', region: 'South Asia', severity: 7, - icon: 'πŸ—', + icon: Construction, tag: 'PORT STRIKE', tagColor: 'var(--accent-blue)', preview: '4,800 dockworkers AWOL. 5M TEU/yr port offline. Mundra at 85% capacity.', @@ -48,14 +66,14 @@ const SCENARIOS = [ ]; const STAGES = [ - { id: 'idle', label: 'Ready', icon: 'β—Ž', color: 'var(--text-muted)' }, - { id: 'injected', label: 'Disruption Injected',icon: '⚑', color: 'var(--accent-amber)' }, - { id: 'monitoring',label: 'Monitor Agent', icon: 'πŸ“‘', color: 'var(--accent-blue)' }, - { id: 'impact', label: 'Impact Analysis', icon: 'πŸ“Š', color: 'var(--accent-red)' }, - { id: 'resolution',label: 'AI Resolution', icon: 'πŸ€–', color: 'var(--accent-cyan)' }, - { id: 'decision', label: 'Human Decision', icon: 'βš–οΈ', color: 'var(--accent-amber)' }, - { id: 'applied', label: 'Protocol Applied', icon: 'βœ“', color: 'var(--accent-green)' }, - { id: 'report', label: 'Report Ready', icon: 'πŸ“„', color: 'var(--accent-cyan)' }, + { id: 'idle', label: 'Ready', icon: Circle, color: 'var(--text-muted)' }, + { id: 'injected', label: 'Disruption Injected',icon: Zap, color: 'var(--accent-amber)' }, + { id: 'monitoring',label: 'Monitor Agent', icon: Radio, color: 'var(--accent-blue)' }, + { id: 'impact', label: 'Impact Analysis', icon: BarChart3, color: 'var(--accent-red)' }, + { id: 'resolution',label: 'AI Resolution', icon: Bot, color: 'var(--accent-cyan)' }, + { id: 'decision', label: 'Human Decision', icon: Scale, color: 'var(--accent-amber)' }, + { id: 'applied', label: 'Protocol Applied', icon: CheckCircle2, color: 'var(--accent-green)' }, + { id: 'report', label: 'Report Ready', icon: FileText, color: 'var(--accent-cyan)' }, ]; const STAGE_INDEX = Object.fromEntries(STAGES.map((s, i) => [s.id, i])); @@ -114,11 +132,14 @@ function StageNode({ stage, index, currentIndex }) { flexShrink: 0, }} > - {done ? 'βœ“' : stage.icon} + {(() => { + const Icon = done ? CheckCircle2 : stage.icon; + return ; + })()}
- + {label} {value} @@ -266,7 +287,7 @@ function OptionCardInline({ option, onApprove, isApproving, approvedRank }) {
-
+
AI Confidence
@@ -342,7 +363,7 @@ function OptionCardInline({ option, onApprove, isApproving, approvedRank }) { textTransform: 'uppercase', }} > - βœ“ Protocol Deployed + Protocol Deployed
) : (
)} @@ -761,10 +782,15 @@ export default function DemoPage() { : 'none', }} > -
{sc.icon}
+
+ {(() => { + const Icon = sc.icon; + return ; + })()} +
{sc.preview}
@@ -817,7 +843,15 @@ export default function DemoPage() { boxShadow: selectedScenario ? '0 0 24px rgba(34,211,238,0.3)' : 'none', }} > - {launching ? '⚑ Injecting…' : '⚑ Launch Demo Pipeline'} + {launching ? ( + + Injecting… + + ) : ( + + Launch Demo Pipeline + + )}
)} @@ -886,7 +920,7 @@ export default function DemoPage() {
{disruption.severity != null && (
-
+
Severity Score
@@ -976,9 +1010,9 @@ export default function DemoPage() { }} > {stage === 'report' - ? 'βœ“ Protocol Applied β€” Report Ready' + ? Protocol Applied β€” Report Ready : stage === 'applied' - ? '⚑ Executing Protocol…' + ? Executing Protocol… : stage === 'decision' ? 'Human Decision Required' : 'AI Generated 3 Resolution Strategies'} @@ -1075,7 +1109,9 @@ export default function DemoPage() { marginBottom: 4, }} > - βœ“ Pipeline Complete + + Pipeline Complete +
Executive incident report generated. Download the PDF or run another scenario. @@ -1098,7 +1134,9 @@ export default function DemoPage() { cursor: 'pointer', }} > - πŸ“„ Download PDF + + Download PDF + )}
@@ -1139,7 +1179,7 @@ export default function DemoPage() { >
{activeScenario && (
-
Scenario
-
- {activeScenario.icon} {activeScenario.label} +
Scenario
+
+ {(() => { + const Icon = activeScenario.icon; + return ; + })()} {activeScenario.label}
)} {disruptionId && (
-
Disruption ID
-
+
Disruption ID
+
{disruptionId}
)} {traceId && (
-
Trace ID
-
+
Trace ID
+
{traceId}
)}
-
Stage
-
- {STAGES[currentStageIndex]?.icon} {STAGES[currentStageIndex]?.label} +
Stage
+
+ {(() => { + const Icon = STAGES[currentStageIndex]?.icon; + return Icon ? : null; + })()} {STAGES[currentStageIndex]?.label}
@@ -1203,7 +1249,7 @@ export default function DemoPage() { gap: 8, }} > - + Agent Console {stage !== 'idle' && stage !== 'report' && } @@ -1238,14 +1284,16 @@ export default function DemoPage() { background: 'var(--bg-elevated)', color: 'var(--text-muted)', fontWeight: 700, - fontSize: 10, + fontSize: 11, letterSpacing: '0.1em', textTransform: 'uppercase', border: '1px solid var(--border-subtle)', cursor: 'pointer', }} > - βœ• Abort & Reset + + Abort & Reset + )}
diff --git a/dashboard/app/developers/page.js b/dashboard/app/developers/page.js index 56ddb11..efd10e9 100644 --- a/dashboard/app/developers/page.js +++ b/dashboard/app/developers/page.js @@ -1,7 +1,7 @@ 'use client'; import { useMemo, useState } from 'react'; -import { Copy, Check, Terminal, Zap, Key, Webhook, Package, AlertTriangle } from 'lucide-react'; +import { Copy, Check, Terminal, Zap, Key, Webhook, Package, AlertTriangle, ChevronUp, ChevronDown } from 'lucide-react'; import NavBar from '../components/NavBar.jsx'; const FALLBACK_BASE = 'https://your-deployed-url.vercel.app'; @@ -51,23 +51,25 @@ function EndpointCard({ method, path, description, example, response }) { onClick={() => setOpen((value) => !value)} className="w-full flex items-center gap-4 p-4 bg-[var(--bg-elevated)]/40 hover:bg-[var(--bg-elevated)] transition-colors text-left" > - {method} - {path} - {description} - {open ? 'β–²' : 'β–Ό'} + {method} + {path} + {description} + + {open ? : } + {open && (
-

{description}

+

{description}

{example && (
-

curl example

+

curl example

)} {response && (
-

response shape

+

response shape

)} diff --git a/dashboard/app/globals.css b/dashboard/app/globals.css index 1515af6..279e1cb 100644 --- a/dashboard/app/globals.css +++ b/dashboard/app/globals.css @@ -46,6 +46,14 @@ /* ── Scrollbar ── */ --scrollbar-thumb: rgba(255, 255, 255, 0.12); --scrollbar-track: transparent; + + /* ── Typography floor ── */ + --text-xs: 11px; + --text-sm: 13px; + --text-base: 14px; + --text-md: 15px; + --tracking-wide: 0.12em; + --tracking-wider: 0.18em; } [data-theme="dark"] { @@ -91,6 +99,14 @@ /* ── Scrollbar ── */ --scrollbar-thumb: rgba(255, 255, 255, 0.12); --scrollbar-track: transparent; + + /* ── Typography floor ── */ + --text-xs: 11px; + --text-sm: 13px; + --text-base: 14px; + --text-md: 15px; + --tracking-wide: 0.12em; + --tracking-wider: 0.18em; } /* ── Light theme ── */ @@ -137,6 +153,14 @@ /* ── Scrollbar ── */ --scrollbar-thumb: rgba(15, 23, 42, 0.12); --scrollbar-track: transparent; + + /* ── Typography floor ── */ + --text-xs: 11px; + --text-sm: 13px; + --text-base: 14px; + --text-md: 15px; + --tracking-wide: 0.12em; + --tracking-wider: 0.18em; } /* ── Globe page always stays dark regardless of theme ── */ @@ -181,6 +205,14 @@ /* ── Scrollbar ── */ --scrollbar-thumb: rgba(255,255,255,0.15); --scrollbar-track: transparent; + + /* ── Typography floor ── */ + --text-xs: 11px; + --text-sm: 13px; + --text-base: 14px; + --text-md: 15px; + --tracking-wide: 0.12em; + --tracking-wider: 0.18em; } @theme inline { @@ -394,3 +426,15 @@ body { opacity: 1; pointer-events: auto; } + +/* ── Shimmer Animation for Skeletons ── */ +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.animate-shimmer { + background: linear-gradient(90deg, transparent 25%, rgba(255,255,255,0.05) 50%, transparent 75%); + background-size: 200% 100%; + animation: shimmer 2s infinite linear; +} diff --git a/dashboard/app/layout.js b/dashboard/app/layout.js index e86960f..13d99fc 100644 --- a/dashboard/app/layout.js +++ b/dashboard/app/layout.js @@ -38,10 +38,8 @@ export const viewport = { export default function RootLayout({ children }) { return ( - -