Files
boc/packages/ui/src/pages/FieldConsole.tsx
T
Bernt e669155bc9 MVP-0: Field Console + Health Dashboard
- Field Console: 4 tabs (Upload, Queue, Artifact Viewer, Timeline)
- Health Dashboard: system pulse for 6 engines
- No more ADR documents until MVP-0 proven with real mission

Stoppregel: Ingen ny arkitektur förrän första verkliga uppdraget.

Next: Pilot 001 — First real field upload
2026-07-02 16:22:21 +00:00

264 lines
7.8 KiB
TypeScript

/**
* 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<Mission[]>([]);
useEffect(() => {
fetch('/api/v1/missions')
.then(r => r.json())
.then(data => setMissions(data));
}, []);
return (
<div>
<h2>Field Console</h2>
{/* Tabs */}
<div style={{ display: 'flex', gap: 8, marginBottom: 20, borderBottom: '2px solid #333' }}>
{(['upload', 'queue', 'viewer', 'timeline'] as const).map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
style={{
padding: '8px 16px',
background: activeTab === tab ? '#333' : 'transparent',
color: activeTab === tab ? 'white' : '#333',
border: 'none',
cursor: 'pointer',
textTransform: 'capitalize',
}}
>
{tab}
</button>
))}
</div>
{/* Content */}
{activeTab === 'upload' && <UploadView />}
{activeTab === 'queue' && <QueueView missions={missions} />}
{activeTab === 'viewer' && <ArtifactViewerView missions={missions} />}
{activeTab === 'timeline' && <TimelineView missions={missions} />}
</div>
);
}
// 1. Upload View
function UploadView() {
return (
<div>
<h3>Upload</h3>
<div style={{ display: 'grid', gap: 16, maxWidth: 400 }}>
<UploadCard icon="📱" label="Phone" />
<UploadCard icon="🎥" label="Video" />
<UploadCard icon="📷" label="Image" />
<UploadCard icon="📍" label="GPS" />
<button style={{
padding: '16px 24px',
background: '#1e90ff',
color: 'white',
border: 'none',
borderRadius: 8,
fontSize: 18,
cursor: 'pointer',
}}>
Import
</button>
</div>
</div>
);
}
function UploadCard({ icon, label }: { icon: string; label: string }) {
return (
<div style={{
border: '2px dashed #ccc',
padding: 24,
borderRadius: 8,
textAlign: 'center',
cursor: 'pointer',
}}>
<span style={{ fontSize: 32 }}>{icon}</span>
<p style={{ margin: '8px 0 0', color: '#666' }}>{label}</p>
</div>
);
}
// 2. Queue View
function QueueView({ missions }: { missions: Mission[] }) {
return (
<div>
<h3>Queue</h3>
{missions.map(mission => (
<div key={mission.id} style={{ border: '1px solid #ccc', padding: 16, borderRadius: 8, marginBottom: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<strong>{mission.id}</strong>
<span style={{ color: '#666' }}>{mission.status}</span>
</div>
<ProgressBar progress={mission.progress} />
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 8, fontSize: 12 }}>
<StatusBadge label="Uploading" done={mission.steps.uploaded} />
<StatusBadge label="Metadata" done={mission.steps.metadataExtracted} />
<StatusBadge label="Knowledge" done={mission.steps.knowledgeExtracted} />
<StatusBadge label="Decision" done={mission.steps.decisionReady} />
</div>
</div>
))}
</div>
);
}
function ProgressBar({ progress }: { progress: number }) {
return (
<div style={{ height: 8, background: '#f0f0f0', borderRadius: 4, overflow: 'hidden' }}>
<div style={{
width: `${progress}%`,
height: '100%',
background: '#1e90ff',
transition: 'width 0.3s',
}} />
</div>
);
}
function StatusBadge({ label, done }: { label: string; done: boolean }) {
return (
<span style={{
padding: '4px 8px',
borderRadius: 4,
background: done ? '#32cd32' : '#ffd700',
color: 'white',
fontSize: 12,
}}>
{done ? '✓' : '⏳'} {label}
</span>
);
}
// 3. Artifact Viewer View
function ArtifactViewerView({ missions }: { missions: Mission[] }) {
const [selectedMission, setSelectedMission] = useState<Mission | null>(missions[0] || null);
if (!selectedMission) return <div>No missions yet</div>;
return (
<div>
<h3>Artifact Viewer</h3>
{/* Mission selector */}
<select
value={selectedMission.id}
onChange={e => {
const mission = missions.find(m => m.id === e.target.value);
if (mission) setSelectedMission(mission);
}}
style={{ marginBottom: 16, padding: 8 }}
>
{missions.map(m => (
<option key={m.id} value={m.id}>{m.id}</option>
))}
</select>
<div style={{ display: 'grid', gap: 16 }}>
{/* Original */}
<div style={{ border: '1px solid #ccc', padding: 16, borderRadius: 8 }}>
<h4>Original</h4>
<div style={{
background: '#f0f0f0',
height: 200,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 4,
}}>
<span style={{ fontSize: 48 }}>🎥</span>
</div>
</div>
{/* Metadata */}
<div style={{ border: '1px solid #ccc', padding: 16, borderRadius: 8 }}>
<h4>Metadata</h4>
<p><strong>GPS:</strong> 59.3293, 18.0686</p>
<p><strong>Hash:</strong> <code>sha256:abc123...</code></p>
<p><strong>Storage:</strong> <code>file:///uploads/...</code></p>
</div>
{/* Knowledge */}
<div style={{ border: '1px solid #ccc', padding: 16, borderRadius: 8 }}>
<h4>Knowledge</h4>
<p><strong>Objects:</strong> Pending</p>
<p><strong>Ontology:</strong> Pending</p>
</div>
{/* History */}
<div style={{ border: '1px solid #ccc', padding: 16, borderRadius: 8 }}>
<h4>History</h4>
<p>Created: {new Date(selectedMission.createdAt).toLocaleString()}</p>
</div>
</div>
</div>
);
}
// 4. Processing Timeline View
function TimelineView({ missions }: { missions: Mission[] }) {
return (
<div>
<h3>Processing Timeline</h3>
{missions.map(mission => (
<div key={mission.id} style={{ border: '1px solid #ccc', padding: 16, borderRadius: 8, marginBottom: 16 }}>
<h4>{mission.id}</h4>
<TimelineStep label="Uploaded" time="09:41" done={mission.steps.uploaded} />
<TimelineStep label="Archived" time="09:41" done={mission.steps.archived} />
<TimelineStep label="Metadata" time="09:42" done={mission.steps.metadataExtracted} />
<TimelineStep label="Knowledge" time="09:43" done={mission.steps.knowledgeExtracted} />
<TimelineStep label="Decision" time="Pending" done={mission.steps.decisionReady} />
</div>
))}
</div>
);
}
function TimelineStep({ label, time, done }: { label: string; time: string; done: boolean }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
<span style={{
width: 12,
height: 12,
borderRadius: '50%',
background: done ? '#32cd32' : '#ccc',
}} />
<span style={{ minWidth: 100, color: '#666' }}>{time}</span>
<span style={{ color: done ? '#333' : '#999' }}>{label}</span>
</div>
);
}
export default FieldConsole;