diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 460d173c9..8cd786bca 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -2,6 +2,8 @@ import { Routes, Route, Link } from 'react-router-dom'; import DatasetExplorer from './pages/DatasetExplorer'; import ArtifactViewer from './pages/ArtifactViewer'; import MissionUpload from './pages/MissionUpload'; +import FieldConsole from './pages/FieldConsole'; +import HealthDashboard from './pages/HealthDashboard'; function App() { return ( @@ -11,6 +13,8 @@ function App() { @@ -18,6 +22,8 @@ function App() { } /> } /> } /> + } /> + } /> ); diff --git a/packages/ui/src/pages/FieldConsole.tsx b/packages/ui/src/pages/FieldConsole.tsx new file mode 100644 index 000000000..e2da2b3ae --- /dev/null +++ b/packages/ui/src/pages/FieldConsole.tsx @@ -0,0 +1,263 @@ +/** + * Field Console + * + * MVP-0: First Field Upload + * + * Four views: + * 1. Upload — phone, video, image, GPS + * 2. Queue — mission status + * 3. Artifact Viewer — original, metadata, knowledge, history + * 4. Processing Timeline — step-by-step progress + */ + +import { useState, useEffect } from 'react'; + +interface Mission { + id: string; + status: string; + progress: number; + createdAt: string; + steps: { + uploaded: boolean; + archived: boolean; + metadataExtracted: boolean; + knowledgeExtracted: boolean; + decisionReady: boolean; + }; +} + +function FieldConsole() { + const [activeTab, setActiveTab] = useState<'upload' | 'queue' | 'viewer' | 'timeline'>('upload'); + const [missions, setMissions] = useState([]); + + useEffect(() => { + fetch('/api/v1/missions') + .then(r => r.json()) + .then(data => setMissions(data)); + }, []); + + return ( +
+

Field Console

+ + {/* Tabs */} +
+ {(['upload', 'queue', 'viewer', 'timeline'] as const).map(tab => ( + + ))} +
+ + {/* Content */} + {activeTab === 'upload' && } + {activeTab === 'queue' && } + {activeTab === 'viewer' && } + {activeTab === 'timeline' && } +
+ ); +} + +// 1. Upload View +function UploadView() { + return ( +
+

Upload

+
+ + + + + +
+
+ ); +} + +function UploadCard({ icon, label }: { icon: string; label: string }) { + return ( +
+ {icon} +

{label}

+
+ ); +} + +// 2. Queue View +function QueueView({ missions }: { missions: Mission[] }) { + return ( +
+

Queue

+ {missions.map(mission => ( +
+
+ {mission.id} + {mission.status} +
+ +
+ + + + +
+
+ ))} +
+ ); +} + +function ProgressBar({ progress }: { progress: number }) { + return ( +
+
+
+ ); +} + +function StatusBadge({ label, done }: { label: string; done: boolean }) { + return ( + + {done ? '✓' : '⏳'} {label} + + ); +} + +// 3. Artifact Viewer View +function ArtifactViewerView({ missions }: { missions: Mission[] }) { + const [selectedMission, setSelectedMission] = useState(missions[0] || null); + + if (!selectedMission) return
No missions yet
; + + return ( +
+

Artifact Viewer

+ + {/* Mission selector */} + + +
+ {/* Original */} +
+

Original

+
+ 🎥 +
+
+ + {/* Metadata */} +
+

Metadata

+

GPS: 59.3293, 18.0686

+

Hash: sha256:abc123...

+

Storage: file:///uploads/...

+
+ + {/* Knowledge */} +
+

Knowledge

+

Objects: Pending

+

Ontology: Pending

+
+ + {/* History */} +
+

History

+

Created: {new Date(selectedMission.createdAt).toLocaleString()}

+
+
+
+ ); +} + +// 4. Processing Timeline View +function TimelineView({ missions }: { missions: Mission[] }) { + return ( +
+

Processing Timeline

+ {missions.map(mission => ( +
+

{mission.id}

+ + + + + +
+ ))} +
+ ); +} + +function TimelineStep({ label, time, done }: { label: string; time: string; done: boolean }) { + return ( +
+ + {time} + {label} +
+ ); +} + +export default FieldConsole; diff --git a/packages/ui/src/pages/HealthDashboard.tsx b/packages/ui/src/pages/HealthDashboard.tsx new file mode 100644 index 000000000..edbdb1359 --- /dev/null +++ b/packages/ui/src/pages/HealthDashboard.tsx @@ -0,0 +1,106 @@ +/** + * Health Dashboard + * + * System pulse. Not BI. Not statistics. + * Just: which engines are running? + */ + +import { useEffect, useState } from 'react'; + +interface EngineStatus { + name: string; + status: 'running' | 'stopped' | 'error'; + lastHeartbeat: string; +} + +function HealthDashboard() { + const [engines, setEngines] = useState([ + { name: 'Reality', status: 'running', lastHeartbeat: new Date().toISOString() }, + { name: 'Knowledge', status: 'running', lastHeartbeat: new Date().toISOString() }, + { name: 'Decision', status: 'stopped', lastHeartbeat: '-' }, + { name: 'Mission', status: 'stopped', lastHeartbeat: '-' }, + { name: 'Economic', status: 'running', lastHeartbeat: new Date().toISOString() }, + { name: 'Learning', status: 'stopped', lastHeartbeat: '-' }, + ]); + + useEffect(() => { + // Poll health status + const interval = setInterval(() => { + fetch('/health') + .then(r => r.json()) + .then(data => { + // Update engine status based on health response + setEngines(prev => prev.map(engine => ({ + ...engine, + status: data.status === 'ok' ? 'running' : 'error', + lastHeartbeat: new Date().toISOString(), + }))); + }) + .catch(() => { + setEngines(prev => prev.map(engine => ({ + ...engine, + status: 'error', + }))); + }); + }, 30000); // Every 30 seconds + + return () => clearInterval(interval); + }, []); + + return ( +
+

Health Dashboard

+

System pulse — not statistics

+ +
+ {engines.map(engine => ( +
+
+

{engine.name} Engine

+

+ Last heartbeat: {engine.lastHeartbeat === '-' ? '-' : new Date(engine.lastHeartbeat).toLocaleTimeString()} +

+
+ +
+ ))} +
+
+ ); +} + +function StatusIndicator({ status }: { status: 'running' | 'stopped' | 'error' }) { + const colors = { + running: '#32cd32', + stopped: '#ccc', + error: '#dc143c', + }; + + const labels = { + running: '✓ Running', + stopped: '○ Stopped', + error: '✗ Error', + }; + + return ( + + {labels[status]} + + ); +} + +export default HealthDashboard;