landvex: Fixar och tester klara för alla komponenter

- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
This commit is contained in:
Bernt
2026-07-05 06:41:32 +00:00
parent f4f853d94b
commit aee0f09db8
19583 changed files with 1450867 additions and 1153 deletions
+3
View File
@@ -8,6 +8,7 @@ import HealthDashboard from './pages/HealthDashboard';
import PilotChecklist from './pages/PilotChecklist';
import ReadinessDashboard from './pages/ReadinessDashboard';
import KillList from './pages/KillList';
import GovernmentPortal from './pages/GovernmentPortal';
function App() {
const [developerMode, setDeveloperMode] = useState(false);
@@ -68,6 +69,7 @@ function App() {
<Link to="/pilot" onClick={() => setMenuOpen(false)} style={{ padding: '8px 0', textDecoration: 'none', color: '#333' }}> Pilot</Link>
<Link to="/readiness" onClick={() => setMenuOpen(false)} style={{ padding: '8px 0', textDecoration: 'none', color: '#333' }}>📋 Ready</Link>
<Link to="/kill" onClick={() => setMenuOpen(false)} style={{ padding: '8px 0', textDecoration: 'none', color: '#333' }}>🔥 Kill</Link>
<Link to="/government" onClick={() => setMenuOpen(false)} style={{ padding: '8px 0', textDecoration: 'none', color: '#333' }}>🏛 Gov</Link>
{developerMode && (
<>
<Link to="/datasets" onClick={() => setMenuOpen(false)} style={{ padding: '8px 0', textDecoration: 'none', color: '#666' }}>🗂 Data</Link>
@@ -88,6 +90,7 @@ function App() {
<Route path="/pilot" element={<PilotChecklist />} />
<Route path="/readiness" element={<ReadinessDashboard />} />
<Route path="/kill" element={<KillList />} />
<Route path="/government" element={<GovernmentPortal />} />
</Routes>
</div>
);
+291
View File
@@ -0,0 +1,291 @@
import { useState, useEffect } from 'react';
interface GovernmentProfile {
name: string;
orgNumber: string;
address: string;
contactEmail: string;
contactPhone: string;
}
interface BudgetOverview {
monthlyLimit: number;
yearlyLimit: number;
currentSpend: number;
forecast: number;
alerts: string[];
}
interface PaymentSettings {
method: 'card' | 'invoice' | 'autogiro';
cardLast4?: string;
invoiceAddress?: string;
invoiceReference?: string;
}
const API_BASE = (import.meta as any).env?.VITE_API_URL || 'https://pilot.landvex.com';
export default function GovernmentPortal() {
const [activeTab, setActiveTab] = useState<'profile' | 'budget' | 'payment' | 'areas' | 'users'>('profile');
const [profile, setProfile] = useState<GovernmentProfile | null>(null);
const [budget, setBudget] = useState<BudgetOverview | null>(null);
const [payment, setPayment] = useState<PaymentSettings>({ method: 'invoice' });
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchProfile();
}, []);
const fetchProfile = async () => {
try {
const res = await fetch(`${API_BASE}/api/v1/government/me`);
if (!res.ok) throw new Error('Failed to load profile');
const data = await res.json();
setProfile(data);
setLoading(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
setLoading(false);
}
};
const fetchBudget = async () => {
try {
const res = await fetch(`${API_BASE}/api/v1/government/budget`);
if (!res.ok) throw new Error('Failed to load budget');
const data = await res.json();
setBudget(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
};
const updatePayment = async () => {
try {
const res = await fetch(`${API_BASE}/api/v1/government/payment`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payment),
});
if (!res.ok) throw new Error('Failed to update payment');
alert('Payment settings updated');
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
};
if (loading) return <div style={{ padding: 40, textAlign: 'center' }}>Loading...</div>;
if (error) return <div style={{ padding: 40, color: '#ef4444' }}>Error: {error}</div>;
const tabs = [
{ id: 'profile' as const, label: 'Profile' },
{ id: 'budget' as const, label: 'Budget' },
{ id: 'payment' as const, label: 'Payment' },
{ id: 'areas' as const, label: 'Watch Areas' },
{ id: 'users' as const, label: 'Users' },
];
return (
<div style={{ maxWidth: 800, margin: '0 auto', padding: 24 }}>
<h1 style={{ fontSize: 28, marginBottom: 8 }}>Government Portal</h1>
<p style={{ color: '#64748b', marginBottom: 24 }}>
Manage your organization's settings, budget, and users
</p>
{/* Tabs */}
<div style={{ display: 'flex', gap: 4, marginBottom: 24, borderBottom: '1px solid #e2e8f0' }}>
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => {
setActiveTab(tab.id);
if (tab.id === 'budget') fetchBudget();
}}
style={{
padding: '12px 20px',
border: 'none',
background: 'none',
borderBottom: activeTab === tab.id ? '2px solid #3b82f6' : '2px solid transparent',
color: activeTab === tab.id ? '#3b82f6' : '#64748b',
fontWeight: activeTab === tab.id ? 600 : 400,
cursor: 'pointer',
fontSize: 14,
}}
>
{tab.label}
</button>
))}
</div>
{/* Profile Tab */}
{activeTab === 'profile' && profile && (
<div style={{ background: '#f8fafc', padding: 24, borderRadius: 12 }}>
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Organization Profile</h2>
<div style={{ display: 'grid', gap: 16 }}>
<div>
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Organization Name</label>
<div style={{ fontSize: 16, fontWeight: 600 }}>{profile.name}</div>
</div>
<div>
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Organization Number</label>
<div style={{ fontSize: 16, fontFamily: 'monospace' }}>{profile.orgNumber}</div>
</div>
<div>
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Address</label>
<div>{profile.address}</div>
</div>
<div>
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Contact Email</label>
<div>{profile.contactEmail}</div>
</div>
<div>
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Contact Phone</label>
<div>{profile.contactPhone}</div>
</div>
</div>
</div>
)}
{/* Budget Tab */}
{activeTab === 'budget' && (
<div style={{ background: '#f8fafc', padding: 24, borderRadius: 12 }}>
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Budget Overview</h2>
{budget ? (
<div style={{ display: 'grid', gap: 16 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div style={{ background: '#fff', padding: 16, borderRadius: 8 }}>
<div style={{ fontSize: 12, color: '#64748b' }}>Monthly Limit</div>
<div style={{ fontSize: 24, fontWeight: 700 }}>${budget.monthlyLimit.toLocaleString()}</div>
</div>
<div style={{ background: '#fff', padding: 16, borderRadius: 8 }}>
<div style={{ fontSize: 12, color: '#64748b' }}>Yearly Limit</div>
<div style={{ fontSize: 24, fontWeight: 700 }}>${budget.yearlyLimit.toLocaleString()}</div>
</div>
</div>
<div style={{ background: '#fff', padding: 16, borderRadius: 8 }}>
<div style={{ fontSize: 12, color: '#64748b' }}>Current Spend</div>
<div style={{ fontSize: 24, fontWeight: 700, color: budget.currentSpend > budget.monthlyLimit * 0.8 ? '#ef4444' : '#22c55e' }}>
${budget.currentSpend.toLocaleString()}
</div>
<div style={{ marginTop: 8, height: 8, background: '#e2e8f0', borderRadius: 4 }}>
<div style={{
width: `${Math.min((budget.currentSpend / budget.monthlyLimit) * 100, 100)}%`,
height: '100%',
background: budget.currentSpend > budget.monthlyLimit * 0.8 ? '#ef4444' : '#3b82f6',
borderRadius: 4,
}} />
</div>
</div>
{budget.alerts.length > 0 && (
<div style={{ background: '#fef3c7', padding: 16, borderRadius: 8 }}>
<div style={{ fontWeight: 600, marginBottom: 8 }}>Alerts</div>
{budget.alerts.map((alert, i) => (
<div key={i} style={{ fontSize: 14, color: '#92400e' }}>{alert}</div>
))}
</div>
)}
</div>
) : (
<div>Loading budget...</div>
)}
</div>
)}
{/* Payment Tab */}
{activeTab === 'payment' && (
<div style={{ background: '#f8fafc', padding: 24, borderRadius: 12 }}>
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Payment Settings</h2>
<div style={{ display: 'grid', gap: 16 }}>
<div>
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Payment Method</label>
<select
value={payment.method}
onChange={e => setPayment({ ...payment, method: e.target.value as any })}
style={{ width: '100%', padding: 10, borderRadius: 6, border: '1px solid #e2e8f0' }}
>
<option value="card">Credit Card</option>
<option value="invoice">Invoice</option>
<option value="autogiro">Autogiro</option>
</select>
</div>
{payment.method === 'invoice' && (
<>
<div>
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Invoice Address</label>
<textarea
value={payment.invoiceAddress || ''}
onChange={e => setPayment({ ...payment, invoiceAddress: e.target.value })}
style={{ width: '100%', padding: 10, borderRadius: 6, border: '1px solid #e2e8f0', minHeight: 80 }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Invoice Reference</label>
<input
type="text"
value={payment.invoiceReference || ''}
onChange={e => setPayment({ ...payment, invoiceReference: e.target.value })}
style={{ width: '100%', padding: 10, borderRadius: 6, border: '1px solid #e2e8f0' }}
/>
</div>
</>
)}
<button
onClick={updatePayment}
style={{
padding: '12px 24px',
background: '#3b82f6',
color: '#fff',
border: 'none',
borderRadius: 8,
fontWeight: 600,
cursor: 'pointer',
}}
>
Save Payment Settings
</button>
</div>
</div>
)}
{/* Watch Areas Tab */}
{activeTab === 'areas' && (
<div style={{ background: '#f8fafc', padding: 24, borderRadius: 12 }}>
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Watch Areas</h2>
<p style={{ color: '#64748b' }}>Define geographic areas for automatic mission monitoring</p>
<div style={{ marginTop: 16, padding: 40, textAlign: 'center', background: '#fff', borderRadius: 8 }}>
<div style={{ fontSize: 48, marginBottom: 16 }}>
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="1 6 1 22 8 18 16 22 21 18 21 2 16 6 8 2 1 6"/>
<line x1="8" y1="2" x2="8" y2="18"/>
<line x1="16" y1="6" x2="16" y2="22"/>
</svg>
</div>
<p>Map integration coming soon</p>
<p style={{ fontSize: 12, color: '#64748b' }}>Draw polygons on map to define watch areas</p>
</div>
</div>
)}
{/* Users Tab */}
{activeTab === 'users' && (
<div style={{ background: '#f8fafc', padding: 24, borderRadius: 12 }}>
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Users & Permissions</h2>
<p style={{ color: '#64748b' }}>Manage team members and their access levels</p>
<div style={{ marginTop: 16, padding: 40, textAlign: 'center', background: '#fff', borderRadius: 8 }}>
<div style={{ fontSize: 48, marginBottom: 16 }}>
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<p>User management coming soon</p>
<p style={{ fontSize: 12, color: '#64748b' }}>Invite users, assign roles, manage permissions</p>
</div>
</div>
)}
</div>
);
}