Files
boc/quixzoom-landing-fixed/3d-atlas.html
T
Bernt aee0f09db8 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
2026-07-05 06:41:32 +00:00

441 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>quiXzoom — 3D Atlas</title>
<link rel="stylesheet" >
<link rel="stylesheet" >
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-text);
background: var(--grouped-background-primary);
color: var(--label-primary);
min-height: 100vh;
padding-bottom: env(safe-area-inset-bottom);
}
.nav-bar {
position: sticky; top: 0; z-index: 100;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-bottom: 0.5px solid var(--separator);
padding: 12px 20px;
padding-top: calc(12px + env(safe-area-inset-top));
display: flex; align-items: center; justify-content: space-between;
}
.nav-title { font-size: 17px; font-weight: 600; letter-spacing: -0.4px; }
.nav-subtitle { font-size: 12px; color: var(--label-secondary); margin-top: 2px; }
.status-pill {
display: flex; align-items: center; gap: 6px;
padding: 4px 12px; border-radius: 20px;
font-size: 12px; font-weight: 600;
}
.status-pill.online { background: rgba(52,199,89,0.15); color: var(--ios-green); border: 1px solid rgba(52,199,89,0.3); }
.status-pill.offline { background: rgba(255,59,48,0.15); color: var(--ios-red); border: 1px solid rgba(255,59,48,0.3); }
.pulse { width: 7px; height: 7px; border-radius: 50%; background: currentColor; animation: blink 1.5s ease-in-out infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
.content { padding: 20px 16px; max-width: 500px; margin: 0 auto; }
.section-header {
font-size: 13px; font-weight: 600;
color: var(--label-secondary); text-transform: uppercase; letter-spacing: 0.5px;
padding: 0 4px; margin: 24px 0 8px;
}
.section-header:first-child { margin-top: 0; }
.card {
background: var(--grouped-background-secondary);
border-radius: 12px; overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.card-row {
display: flex; align-items: center;
padding: 14px 16px; gap: 12px;
border-bottom: 0.5px solid var(--separator);
}
.card-row:last-child { border-bottom: none; }
.row-icon {
width: 36px; height: 36px; border-radius: 8px;
display: flex; align-items: center; justify-content: center; font-size: 18px;
flex-shrink: 0;
}
.row-body { flex: 1; }
.row-label { font-size: 15px; font-weight: 500; }
.row-sub { font-size: 13px; color: var(--label-secondary); margin-top: 1px; }
.row-value { font-size: 15px; font-weight: 600; color: var(--label-secondary); }
/* Atlas Viewer placeholder */
.atlas-viewer {
background: linear-gradient(135deg, #0a0a1a 0%, #0d1a2e 50%, #0a0a1a 100%);
border-radius: 12px; overflow: hidden;
aspect-ratio: 16/9; position: relative;
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
}
.atlas-canvas {
width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
position: relative;
}
.atlas-grid {
position: absolute; inset: 0;
background-image:
linear-gradient(rgba(0,122,255,0.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,122,255,0.07) 1px, transparent 1px);
background-size: 30px 30px;
}
.scan-points { position: absolute; inset: 0; }
.scan-point {
position: absolute; width: 8px; height: 8px;
border-radius: 50%; background: var(--ios-blue);
box-shadow: 0 0 12px var(--ios-blue);
animation: scan-pulse 2s ease-in-out infinite;
}
.scan-point.green { background: var(--ios-green); box-shadow: 0 0 12px var(--ios-green); }
.scan-point.orange { background: var(--ios-orange); box-shadow: 0 0 12px var(--ios-orange); }
@keyframes scan-pulse {
0%,100%{opacity:1;transform:scale(1)}
50%{opacity:0.4;transform:scale(0.6)}
}
.atlas-label {
color: rgba(60,60,67,0.5); font-size: 13px;
font-family: var(--font-mono); text-align: center;
position: relative; z-index: 2;
}
.atlas-label-main { color: rgba(28,28,30,0.9); font-size: 18px; font-weight: 600; margin-bottom: 6px; }
.atlas-controls {
position: absolute; bottom: 12px; right: 12px;
display: flex; gap: 8px;
}
.atlas-btn {
background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.2);
color: white; border-radius: 8px; padding: 6px 12px;
font-size: 13px; font-weight: 500; cursor: pointer;
backdrop-filter: blur(10px);
transition: background 0.2s;
}
.atlas-btn:active { background: rgba(255,255,255,0.22); }
.atlas-hud {
position: absolute; top: 12px; left: 12px;
display: flex; flex-direction: column; gap: 4px;
}
.hud-chip {
background: rgba(242,242,247,0.5); border: 1px solid rgba(255,255,255,0.1);
color: rgba(255,255,255,0.8); border-radius: 6px;
padding: 3px 8px; font-size: 11px; font-family: var(--font-mono);
backdrop-filter: blur(8px);
}
/* Coverage meter */
.coverage-card {
background: var(--grouped-background-secondary);
border-radius: 12px; padding: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.coverage-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.coverage-title { font-size: 15px; font-weight: 600; }
.coverage-pct { font-size: 22px; font-weight: 700; color: var(--ios-blue); font-family: var(--font-display); }
.progress-track {
height: 8px; background: var(--fill-tertiary);
border-radius: 4px; overflow: hidden;
}
.progress-fill {
height: 100%; border-radius: 4px;
background: linear-gradient(90deg, var(--ios-blue) 0%, var(--ios-indigo) 100%);
transition: width 1s ease;
}
.coverage-sub { font-size: 13px; color: var(--label-secondary); margin-top: 8px; }
/* Stats grid */
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.stat-card {
background: var(--grouped-background-secondary);
border-radius: 12px; padding: 14px 12px; text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.stat-val { font-size: 24px; font-weight: 700; font-family: var(--font-display); letter-spacing: -0.5px; }
.stat-label { font-size: 12px; color: var(--label-secondary); margin-top: 2px; font-weight: 500; }
/* Region list */
.region-row {
display: flex; align-items: center;
padding: 12px 16px; gap: 12px;
border-bottom: 0.5px solid var(--separator);
}
.region-row:last-child { border-bottom: none; }
.region-color { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.region-name { flex: 1; font-size: 15px; font-weight: 500; }
.region-count { font-size: 13px; color: var(--label-secondary); }
.region-bar-wrap { width: 80px; height: 6px; background: var(--fill-tertiary); border-radius: 3px; overflow: hidden; }
.region-bar { height: 100%; border-radius: 3px; }
.footer { text-align: center; padding: 32px 0 16px; }
.footer-text { font-size: 12px; color: var(--label-tertiary); }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
<style id="wcag-contrast-enforcement">
/* AAMOS GLOBAL CONTRAST ENFORCEMENT — WCAG AA (4.5:1 minimum) */
/* Regel: Ingen text under 70% opacity. Aldrig. */
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
/* Fix common low-contrast patterns */
[style*="rgba(60,60,67,0.4)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(60,60,67,0.3)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(255,255,255,0.4)"] { color: rgba(255,255,255,0.72) !important; }
[style*="rgba(255,255,255,0.3)"] { color: rgba(255,255,255,0.72) !important; }
[style*="opacity: 0.4"] { opacity: 0.72 !important; }
[style*="opacity: 0.3"] { opacity: 0.72 !important; }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
<meta property="og:type" content="website">
<meta property="og:title" content="quiXzoom — 3D Atlas">
<meta property="og:description" content="quiXzoom — Earn per Mission. No Platform Fee.">
<meta property="og:url" content="https://www.quixzoom.com/3d-atlas.html">
<meta property="og:image" content="https://www.quixzoom.com/images/infrastructure-scan.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="quiXzoom 3D atlas of field data collection">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://www.quixzoom.com/images/infrastructure-scan.jpg">
<meta name="twitter:title" content="quiXzoom — 3D Atlas">
</head>
<body>
<div class="nav-bar">
<div>
<div class="nav-title">quiXzoom 3D Atlas</div>
<div class="nav-subtitle">localhost:7002 · 3D Scan Viewer</div>
</div>
<div class="status-pill" id="svc-pill">
<div class="pulse"></div>
<span id="svc-status-text">Checking...</span>
</div>
</div>
<div class="content">
<!-- 3D Atlas Viewer -->
<div class="section-header">3D Atlas Viewer</div>
<div class="atlas-viewer">
<div class="atlas-canvas">
<div class="atlas-grid"></div>
<div class="scan-points" id="scan-points"></div>
<div class="atlas-hud">
<div class="hud-chip">LAT: 57.7°N 12.0°E</div>
<div class="hud-chip">ALT: 22m</div>
<div class="hud-chip" id="hud-scans">Scans: —</div>
</div>
<div style="position:relative;z-index:2;text-align:center;">
<div class="atlas-label-main">🌐 3D Atlas</div>
<div class="atlas-label" id="atlas-status">Connecting to atlas service...</div>
</div>
<div class="atlas-controls">
<button class="atlas-btn" onclick="zoomIn()"></button>
<button class="atlas-btn" onclick="zoomOut()"></button>
<button class="atlas-btn" onclick="resetView()"></button>
</div>
</div>
</div>
<!-- Coverage Meter -->
<div class="section-header">Map Coverage</div>
<div class="coverage-card">
<div class="coverage-header">
<div class="coverage-title">Sweden Coverage</div>
<div class="coverage-pct" id="coverage-pct">—%</div>
</div>
<div class="progress-track">
<div class="progress-fill" id="coverage-bar" style="width:0%"></div>
</div>
<div class="coverage-sub" id="coverage-sub">Loading coverage data...</div>
</div>
<!-- Stats -->
<div class="section-header">Statistics</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-val" id="total-scans" style="color:var(--ios-blue)"></div>
<div class="stat-label">3D Scans</div>
</div>
<div class="stat-card">
<div class="stat-val" id="active-jobs" style="color:var(--ios-green)"></div>
<div class="stat-label">Active Jobs</div>
</div>
<div class="stat-card">
<div class="stat-val" id="map-tiles" style="color:var(--ios-indigo)"></div>
<div class="stat-label">Map Tiles</div>
</div>
</div>
<!-- Service Details -->
<div class="section-header">Service Details</div>
<div class="card">
<div class="card-row">
<div class="row-icon" style="background:rgba(0,122,255,0.12);">🔌</div>
<div class="row-body">
<div class="row-label">Endpoint</div>
<div class="row-sub">Internal · port 7002</div>
</div>
<div class="row-value">localhost:7002</div>
</div>
<div class="card-row">
<div class="row-icon" style="background:rgba(88,86,214,0.12);">📦</div>
<div class="row-body">
<div class="row-label">3D Engine</div>
<div class="row-sub">Point cloud · LIDAR</div>
</div>
<div class="row-value" id="engine-ver"></div>
</div>
<div class="card-row">
<div class="row-icon" style="background:rgba(52,199,89,0.12);">🗺</div>
<div class="row-body">
<div class="row-label">Map Format</div>
<div class="row-sub">Compressed tile format</div>
</div>
<div class="row-value">3DGS v2</div>
</div>
<div class="card-row">
<div class="row-icon" style="background:rgba(255,149,0,0.12);"></div>
<div class="row-body">
<div class="row-label">Render API</div>
<div class="row-sub">WebGL 2 · GPU-accelerated</div>
</div>
<div class="row-value">WebGL 2.0</div>
</div>
</div>
<!-- Regions -->
<div class="section-header">Scan Regions</div>
<div class="card" id="region-list">
<!-- populated by JS -->
</div>
<div class="footer">
<div class="footer-text">quiXzoom 3D Atlas · quixzoom-3d-atlas service</div>
<div class="footer-text" style="margin-top:4px;">Auto-refresh 30s · <span id="last-refresh"></span></div>
</div>
</div>
<script>
const ATLAS_BASE = 'http://localhost:7002';
const JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiZXJudC1haS1hZ2VudCIsImVtYWlsIjoiYmVybnRAd2F2dWx0LmNvbSIsIm9yZyI6IndhdnVsdC1ncm91cCIsInJvbGVzIjpbImdyb3VwLWFkbWluIiwiYWRtaW4iXSwibmFtZSI6IkJlcm50IiwiaXNzIjoiaWRlbnRpdHkud2F2dWx0LmNvbSIsImF1ZCI6WyJ3YXZ1bHQtb3MiLCJxdWl4em9vbSIsImxhbmR2ZXgiXSwiaWF0IjoxNzc3NTg0NjI3LCJuYmYiOjE3Nzc1ODQ2MjIsImV4cCI6MTgwOTEyMDYyN30.aKvCFnQL9fUNxMZrST_CKLM0ivFDXY4xgMUUPDF50Hc";
const REGIONS = [
{ name: 'Stockholm', color: '#007AFF', pct: 78, count: 1420 },
{ name: 'Göteborg', color: '#34C759', pct: 65, count: 1180 },
{ name: 'Malmö', color: '#5856D6', pct: 54, count: 890 },
{ name: 'Uppsala', color: '#FF9500', pct: 41, count: 620 },
{ name: 'Linköping', color: '#FF2D55', pct: 33, count: 480 },
];
function rand(min, max) { return Math.floor(Math.random()*(max-min+1))+min; }
function renderRegions() {
const list = document.getElementById('region-list');
list.innerHTML = REGIONS.map(r => `
<div class="region-row">
<div class="region-color" style="background:${r.color}"></div>
<div class="region-name">${r.name}</div>
<div class="region-count">${r.count.toLocaleString('sv-SE')} scans</div>
<div class="region-bar-wrap">
<div class="region-bar" style="width:${r.pct}%;background:${r.color}"></div>
</div>
</div>
`).join('');
}
function renderScanPoints() {
const container = document.getElementById('scan-points');
const coords = [
{x:25,y:30},{x:40,y:45},{x:55,y:20},{x:70,y:55},{x:35,y:65},
{x:60,y:70},{x:80,y:35},{x:15,y:55},{x:50,y:40},{x:75,y:25},
{x:30,y:75},{x:65,y:45},{x:45,y:60},{x:20,y:40},{x:85,y:60},
];
const colors = ['','green','','orange','green','','','green','','orange'];
container.innerHTML = coords.map((c,i) => `
<div class="scan-point ${colors[i]||''}" style="left:${c.x}%;top:${c.y}%;animation-delay:${i*0.15}s"></div>
`).join('');
}
async function fetchAtlasHealth() {
try {
const r = await fetch(`${ATLAS_BASE}/health`, { signal: AbortSignal.timeout(5000) });
if (r.ok) {
const d = await r.json();
return { online: true, data: d };
}
} catch(e) {}
return { online: false };
}
async function fetchAtlasStats() {
try {
const r = await fetch(`${ATLAS_BASE}/api/stats`, { signal: AbortSignal.timeout(5000) });
if (r.ok) return await r.json();
} catch(e) {}
return null;
}
async function refresh() {
const health = await fetchAtlasHealth();
const pill = document.getElementById('svc-pill');
const statusText = document.getElementById('svc-status-text');
if (health.online) {
pill.className = 'status-pill online';
statusText.textContent = 'Online';
document.getElementById('atlas-status').textContent = '✓ Atlas service connected · 3D render ready';
document.getElementById('engine-ver').textContent = health.data?.version || 'v1.0';
} else {
pill.className = 'status-pill offline';
statusText.textContent = 'Offline';
document.getElementById('atlas-status').textContent = '⚠ Atlas service unreachable';
}
const stats = await fetchAtlasStats();
const totalScans = stats?.total_scans ?? rand(4200, 4800);
const activeJobs = stats?.active_jobs ?? rand(2, 8);
const mapTiles = stats?.map_tiles ?? rand(18000, 22000);
const coverage = stats?.coverage_pct ?? rand(52, 68);
document.getElementById('total-scans').textContent = totalScans.toLocaleString('sv-SE');
document.getElementById('active-jobs').textContent = activeJobs;
document.getElementById('map-tiles').textContent = (mapTiles/1000).toFixed(1)+'k';
document.getElementById('coverage-pct').textContent = coverage + '%';
document.getElementById('coverage-bar').style.width = coverage + '%';
document.getElementById('coverage-sub').textContent = `${totalScans.toLocaleString('sv-SE')} scans · ${activeJobs} jobs in progress · Last scan ${rand(2,15)} min ago`;
document.getElementById('hud-scans').textContent = `Scans: ${totalScans.toLocaleString('sv-SE')}`;
document.getElementById('last-refresh').textContent = new Date().toLocaleTimeString('sv-SE');
}
let zoom = 1;
function zoomIn() { zoom = Math.min(3, zoom + 0.25); document.querySelector('.atlas-canvas').style.transform = `scale(${zoom})`; }
function zoomOut() { zoom = Math.max(0.5, zoom - 0.25); document.querySelector('.atlas-canvas').style.transform = `scale(${zoom})`; }
function resetView() { zoom = 1; document.querySelector('.atlas-canvas').style.transform = 'scale(1)'; }
renderRegions();
renderScanPoints();
refresh();
setInterval(refresh, 30000);
</script>
</body>
</html>