184 lines
6.7 KiB
HTML
184 lines
6.7 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>LIFE — Real Data Status</title>
|
||
|
|
<style>
|
||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif; background: #000; color: #fff; line-height: 1.6; }
|
||
|
|
.header { background: #0a0a0a; border-bottom: 1px solid #333; padding: 20px 40px; }
|
||
|
|
.header h1 { font-size: 1.5rem; color: #00d4ff; }
|
||
|
|
.header p { color: #888; font-size: 0.9rem; }
|
||
|
|
.main { padding: 40px; max-width: 1200px; margin: 0 auto; }
|
||
|
|
.section { background: #111; border: 1px solid #333; border-radius: var(--radius-md); padding: 24px; margin-bottom: 24px; }
|
||
|
|
.section h2 { font-size: 1.2rem; margin-bottom: 16px; color: #00d4ff; }
|
||
|
|
.source { display: flex; justify-content: space-between; align-items: center; padding: 12px; border-bottom: 1px solid #222; }
|
||
|
|
.source:last-child { border-bottom: none; }
|
||
|
|
.source-name { font-weight: 600; }
|
||
|
|
.source-type { font-size: 0.8rem; padding: 2px 8px; border-radius: var(--radius-sm); }
|
||
|
|
.type-real { background: #1a3a1a; color: #4ade80; }
|
||
|
|
.type-simulated { background: #3a1a1a; color: #f87171; }
|
||
|
|
.source-count { font-size: 1.2rem; font-weight: 700; }
|
||
|
|
.metric { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||
|
|
.metric-card { background: #111; border: 1px solid #333; border-radius: var(--radius-md); padding: 20px; }
|
||
|
|
.metric-value { font-size: 2rem; font-weight: 700; color: #00d4ff; }
|
||
|
|
.metric-label { font-size: 0.85rem; color: #888; }
|
||
|
|
.status { display: inline-block; width: 8px; height: 8px; border-radius: var(--radius-full); margin-right: 8px; }
|
||
|
|
.status-active { background: #4ade80; }
|
||
|
|
.status-inactive { background: #f87171; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
|
||
|
|
<div class="header">
|
||
|
|
<h1>LIFE — Real Data Status</h1>
|
||
|
|
<p>Honest overview of what data is real vs simulated</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="main">
|
||
|
|
|
||
|
|
<div class="metric">
|
||
|
|
<div class="metric-card">
|
||
|
|
<div class="metric-value" id="real-count">—</div>
|
||
|
|
<div class="metric-label">Real Observations</div>
|
||
|
|
</div>
|
||
|
|
<div class="metric-card">
|
||
|
|
<div class="metric-value" id="sim-count">—</div>
|
||
|
|
<div class="metric-label">Simulated Observations</div>
|
||
|
|
</div>
|
||
|
|
<div class="metric-card">
|
||
|
|
<div class="metric-value" id="real-percent">—</div>
|
||
|
|
<div class="metric-label">Real Data %</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Data Sources</h2>
|
||
|
|
<div id="sources">
|
||
|
|
<div style="color:#666;text-align:center;padding:20px;">Loading...</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Real Data Pipeline</h2>
|
||
|
|
<div class="source">
|
||
|
|
<div>
|
||
|
|
<div class="source-name"><span class="status status-active"></span>SMHI Weather</div>
|
||
|
|
<div style="color:#888;font-size:0.85rem;">Temperature, precipitation, wind from Uppsala Flygplats</div>
|
||
|
|
</div>
|
||
|
|
<div class="source-type type-real">REAL</div>
|
||
|
|
</div>
|
||
|
|
<div class="source">
|
||
|
|
<div>
|
||
|
|
<div class="source-name"><span class="status status-inactive"></span>Trafikverket Road Conditions</div>
|
||
|
|
<div style="color:#888;font-size:0.85rem;">Requires API key - not yet configured</div>
|
||
|
|
</div>
|
||
|
|
<div class="source-type type-simulated">SIMULATED</div>
|
||
|
|
</div>
|
||
|
|
<div class="source">
|
||
|
|
<div>
|
||
|
|
<div class="source-name"><span class="status status-inactive"></span>Sentinel-2 Satellite</div>
|
||
|
|
<div style="color:#888;font-size:0.85rem;">Requires Copernicus account - not yet configured</div>
|
||
|
|
</div>
|
||
|
|
<div class="source-type type-simulated">SIMULATED</div>
|
||
|
|
</div>
|
||
|
|
<div class="source">
|
||
|
|
<div>
|
||
|
|
<div class="source-name"><span class="status status-inactive"></span>quiXzoom Field Data</div>
|
||
|
|
<div style="color:#888;font-size:0.85rem;">No active field contributors in area</div>
|
||
|
|
</div>
|
||
|
|
<div class="source-type type-simulated">SIMULATED</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Latest Real Data</h2>
|
||
|
|
<table style="width:100%;border-collapse:collapse;">
|
||
|
|
<thead>
|
||
|
|
<tr style="border-bottom:1px solid #333;">
|
||
|
|
<th style="text-align:left;padding:8px;color:#888;">Time</th>
|
||
|
|
<th style="text-align:left;padding:8px;color:#888;">Road</th>
|
||
|
|
<th style="text-align:left;padding:8px;color:#888;">Type</th>
|
||
|
|
<th style="text-align:left;padding:8px;color:#888;">Value</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="latest-real">
|
||
|
|
<tr><td colspan="4" style="text-align:center;color:#666;padding:20px;">Loading...</td></tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
async function loadData() {
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/v1/live/public/status');
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
const sources = data.sources;
|
||
|
|
const total = Object.values(sources).reduce((a, b) => a + b, 0);
|
||
|
|
const real = sources['smhi_real'] || 0;
|
||
|
|
const simulated = total - real;
|
||
|
|
|
||
|
|
document.getElementById('real-count').textContent = real;
|
||
|
|
document.getElementById('sim-count').textContent = simulated;
|
||
|
|
document.getElementById('real-percent').textContent = ((real / total) * 100).toFixed(1) + '%';
|
||
|
|
|
||
|
|
// Sources list
|
||
|
|
const container = document.getElementById('sources');
|
||
|
|
container.innerHTML = Object.entries(sources).map(([name, count]) => {
|
||
|
|
const isReal = name === 'smhi_real';
|
||
|
|
return `
|
||
|
|
<div class="source">
|
||
|
|
<div>
|
||
|
|
<div class="source-name">${name}</div>
|
||
|
|
</div>
|
||
|
|
<div style="display:flex;align-items:center;gap:12px;">
|
||
|
|
<div class="source-count">${count}</div>
|
||
|
|
<div class="source-type ${isReal ? 'type-real' : 'type-simulated'}">${isReal ? 'REAL' : 'SIMULATED'}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}).join('');
|
||
|
|
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to load:', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadLatest() {
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/v1/live/public/observations/stream?limit=10');
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
const tbody = document.getElementById('latest-real');
|
||
|
|
const realObs = data.observations.filter(o => o.source === 'smhi_real');
|
||
|
|
|
||
|
|
if (realObs.length === 0) {
|
||
|
|
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:#666;padding:20px;">No real data yet</td></tr>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
tbody.innerHTML = realObs.slice(0, 5).map(obs => `
|
||
|
|
<tr style="border-bottom:1px solid #222;">
|
||
|
|
<td style="padding:8px;">${obs.detected_date}</td>
|
||
|
|
<td style="padding:8px;">${obs.road_name}</td>
|
||
|
|
<td style="padding:8px;">${obs.observation_type}</td>
|
||
|
|
<td style="padding:8px;color:#4ade80;">${obs.severity}</td>
|
||
|
|
</tr>
|
||
|
|
`).join('');
|
||
|
|
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to load latest:', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
loadData();
|
||
|
|
loadLatest();
|
||
|
|
</script>
|
||
|
|
|
||
|
|
</body>
|
||
|
|
</html>
|