Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions dashboard/app/analytics/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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"
>
<p className="text-[11px] uppercase tracking-[0.2em] text-[var(--text-muted)] font-bold">{label}</p>
<p className="text-xs uppercase tracking-[0.2em] text-[var(--text-muted)] font-bold">{label}</p>
<p className="mt-4 font-mono text-4xl font-light text-[var(--text-primary)] leading-none">
{value}
</p>
<div className="mt-4 flex items-center justify-between">
<p className="text-[11px] text-[var(--text-secondary)] font-medium">{sub}</p>
<p className="text-xs text-[var(--text-secondary)] font-medium">{sub}</p>
{trend !== undefined && (
<div className={`flex items-center gap-1 text-xs font-bold ${trend > 0 ? 'text-[var(--accent-green)]' : 'text-[var(--accent-red)]'}`}>
<span className="opacity-70">{trend > 0 ? '▲' : '▼'}</span>
<div className={`flex items-center gap-1 text-sm font-bold ${trend > 0 ? 'text-[var(--accent-green)]' : 'text-[var(--accent-red)]'}`}>
<span className="opacity-70">{trend > 0 ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}</span>
<span>{Math.abs(trend)}%</span>
</div>
)}
Expand All @@ -57,11 +59,11 @@ function ChartPanel({ title, subtitle, children }) {
className="glass-panel p-6 space-y-6"
>
<div className="flex flex-col gap-1">
<p className="text-[11px] font-bold text-[var(--text-secondary)] uppercase tracking-[0.2em] font-display">
<p className="text-xs font-bold text-[var(--text-secondary)] uppercase tracking-[0.2em] font-display">
{title}
</p>
{subtitle && (
<p className="text-[11px] text-[var(--text-muted)] font-medium">{subtitle}</p>
<p className="text-xs text-[var(--text-muted)] font-medium">{subtitle}</p>
)}
</div>
<div className="w-full">
Expand Down Expand Up @@ -115,16 +117,16 @@ export default function AnalyticsPage() {
<div className="flex h-screen flex-col bg-[var(--bg-base)] text-[var(--text-primary)]">
<NavBar />
<div className="flex-1 p-6 space-y-8">
<div className="h-10 w-64 rounded-xl bg-[var(--bg-elevated)] animate-pulse" />
<Skeleton variant="line" className="h-10 w-64 rounded-xl" />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="glass-panel h-32 animate-pulse" />
<Skeleton key={i} variant="block" className="h-32 rounded-2xl" />
))}
</div>
<div className="glass-panel h-72 animate-pulse" />
<Skeleton variant="block" className="h-72 rounded-2xl" />
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div className="glass-panel h-64 animate-pulse" />
<div className="glass-panel h-64 animate-pulse" />
<Skeleton variant="block" className="h-64 rounded-2xl" />
<Skeleton variant="block" className="h-64 rounded-2xl" />
</div>
</div>
</div>
Expand Down Expand Up @@ -164,19 +166,17 @@ export default function AnalyticsPage() {
</div>

<header>
<p className="text-[11px] uppercase tracking-[0.25em] text-[var(--accent-cyan)] font-bold font-display">Operations Intelligence</p>
<p className="text-xs uppercase tracking-[0.25em] text-[var(--accent-cyan)] font-bold font-display">Operations Intelligence</p>
<h1 className="mt-2 text-3xl font-bold tracking-tight font-display">30-Day Performance</h1>
</header>

{(!data || Object.keys(data).length === 0) ? (
<div className="flex-1 flex flex-col items-center justify-center p-12 glass-panel !bg-transparent border-dashed border-2 opacity-60">
<div className="w-16 h-16 rounded-full bg-[var(--bg-elevated)] flex items-center justify-center border border-[var(--border-subtle)] mb-4">
<BarChart3 className="w-8 h-8 text-[var(--text-muted)]" />
</div>
<h3 className="text-sm font-bold text-[var(--text-primary)] uppercase tracking-widest">No Intelligence Data</h3>
<p className="text-[11px] text-[var(--text-secondary)] mt-2 max-w-xs text-center leading-relaxed">
Historical performance metrics are calculated every 24 hours. Check back once your first disruption protocol has been executed.
</p>
<div className="flex-1 min-h-[400px] flex items-center justify-center glass-panel !bg-transparent border-dashed border-2 opacity-60">
<EmptyState
icon={BarChart3}
title="No Intelligence Data"
description="Historical performance metrics are calculated every 24 hours. Check back once your first disruption protocol has been executed."
/>
</div>
) : (
<>
Expand Down
6 changes: 4 additions & 2 deletions dashboard/app/components/AgentPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => <div className="p-4 text-xs text-[var(--text-muted)]">Loading agent reasoning...</div>,
loading: () => <PanelSkeleton />,
});

const NewsFeed = dynamic(() => import('./news/NewsFeed.jsx'), {
ssr: false,
loading: () => <div className="p-4 text-xs text-[var(--text-muted)]">Loading news intelligence...</div>,
loading: () => <PanelSkeleton />,
});

const TABS = [
Expand Down
14 changes: 11 additions & 3 deletions dashboard/app/components/NavBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div key={href} className="flex items-center gap-1">
<div key={href} className="flex items-center">
{showDivider && (
<div className="w-px h-5 bg-[var(--border-subtle)] mx-1 opacity-50" />
<div className="flex items-center">
<div className="w-px h-4 bg-[var(--border-subtle)] mx-2 opacity-50" />
{sectionLabel && (
<span className="text-[9px] text-[var(--text-muted)] font-bold uppercase tracking-[0.2em] mr-2 hidden lg:block">
{sectionLabel}
</span>
)}
</div>
)}
<Link
href={href}
aria-label={label}
title={label}
className={[
'relative flex items-center gap-2 px-4 py-2 rounded-xl text-[10px] font-bold uppercase tracking-[0.1em]',
'relative flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold uppercase tracking-[0.1em]',
'transition-all duration-300 outline-none',
active
? 'bg-[var(--glass-bg-elevated)] text-[var(--text-primary)] shadow-sm border border-[var(--glass-border)]'
Expand Down
104 changes: 52 additions & 52 deletions dashboard/app/components/globe/GlobeView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function GlobeView({ simulationControlsOpen = false, filter: exte
const resetIdleTimerRef = useRef(null);
const [internalFilter, setInternalFilter] = useState('all');
const f = externalFilter ?? internalFilter;
const setF = externalFilter ? () => {} : setInternalFilter;
const setF = externalFilter ? () => { } : setInternalFilter;
const [t, setT] = useState(null);
const [zoomLevel, setZoomLevel] = useState('far');
const [viewerEpoch, setViewerEpoch] = useState(0);
Expand Down Expand Up @@ -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;

Expand All @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -829,17 +829,17 @@ export default function GlobeView({ simulationControlsOpen = false, filter: exte
showSimulationControls={simulationControlsOpen}
/>
<div ref={cRef} className="h-full w-full" />
<div
ref={tooltipRef}
style={{
position: "fixed",
top: 0,
left: 0,
transform: "translate(-9999px, -9999px)",
zIndex: 50,
pointerEvents: "none",
transition: "transform 0.08s ease-out"
}}
<div
ref={tooltipRef}
style={{
position: "fixed",
top: 0,
left: 0,
transform: "translate(-9999px, -9999px)",
zIndex: 50,
pointerEvents: "none",
transition: "transform 0.08s ease-out"
}}
className={`bg-[var(--bg-overlay)] backdrop-blur-xl border border-[var(--border-subtle)] rounded-2xl p-4 shadow-2xl min-w-[160px] ${t ? 'opacity-100 scale-100' : 'opacity-0 scale-95'} transition-all duration-200`}
>
<p className="text-[10px] font-bold uppercase tracking-[0.2em] text-[var(--text-muted)] mb-1 leading-none">{t?.kind || 'Entity'}</p>
Expand Down
27 changes: 27 additions & 0 deletions dashboard/app/components/ui/EmptyState.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export function EmptyState({
icon,
title,
description,
action
}) {
return (
<div className="flex flex-col items-center justify-center py-20 px-8 text-center gap-4">
<div className="w-16 h-16 rounded-2xl bg-[var(--bg-elevated)] flex items-center justify-center text-3xl shadow-sm border border-[var(--border-subtle)]">
{icon}
</div>
<div>
<h3 className="text-[var(--text-primary)] text-base font-bold mb-1">
{title}
</h3>
<p className="text-[13px] text-[var(--text-secondary)] max-w-xs mx-auto leading-relaxed">
{description}
</p>
</div>
{action && (
<div className="mt-2">
{action}
</div>
)}
</div>
);
}
26 changes: 26 additions & 0 deletions dashboard/app/components/ui/GlobeSkeleton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Skeleton } from './Skeleton';

export function GlobeSkeleton() {
return (
<div className="w-full h-full bg-[#020617] flex items-center justify-center">
<div className="flex flex-col items-center gap-6">
<div className="relative">
<div className="w-16 h-16 rounded-full border-2 border-cyan-400/20 border-t-cyan-400 animate-spin" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-8 h-8 rounded-full bg-cyan-400/10 animate-pulse" />
</div>
</div>
<div className="flex flex-col items-center gap-2">
<div className="text-[11px] text-cyan-400/60 uppercase tracking-[0.2em] font-bold animate-pulse">
Initializing Orbital Command
</div>
<div className="flex gap-1.5">
<div className="w-1 h-1 rounded-full bg-cyan-400/20 animate-bounce" style={{ animationDelay: '0ms' }} />
<div className="w-1 h-1 rounded-full bg-cyan-400/40 animate-bounce" style={{ animationDelay: '150ms' }} />
<div className="w-1 h-1 rounded-full bg-cyan-400/20 animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
</div>
</div>
</div>
);
}
25 changes: 25 additions & 0 deletions dashboard/app/components/ui/PanelSkeleton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Skeleton } from './Skeleton';

export function PanelSkeleton() {
return (
<div className="p-6 space-y-6">
<div className="flex items-center gap-3">
<Skeleton variant="circle" className="w-10 h-10" />
<div className="flex-1 space-y-2">
<Skeleton variant="line" className="w-[60%]" />
<Skeleton variant="line" className="w-[40%]" />
</div>
</div>
<div className="space-y-3">
<Skeleton variant="block" className="h-24" />
<Skeleton variant="line" />
<Skeleton variant="line" />
<Skeleton variant="line" className="w-[80%]" />
</div>
<div className="pt-4 flex gap-2">
<Skeleton variant="line" className="h-8 w-24 rounded-lg" />
<Skeleton variant="line" className="h-8 w-24 rounded-lg" />
</div>
</div>
);
}
17 changes: 17 additions & 0 deletions dashboard/app/components/ui/Skeleton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import clsx from 'clsx';

export function Skeleton({ className, variant = 'line', ...props }) {
return (
<div
className={clsx(
'animate-pulse bg-[var(--bg-elevated)] relative overflow-hidden',
'after:absolute after:inset-0 after:-translate-x-full after:animate-shimmer after:bg-gradient-to-r after:from-transparent after:via-white/5 after:to-transparent',
variant === 'line' && 'h-3 w-full rounded-md',
variant === 'block' && 'h-24 w-full rounded-xl',
variant === 'circle' && 'h-12 w-12 rounded-full',
className
)}
{...props}
/>
);
}
Loading
Loading