Files
boc/landvex-site/landvex-admin.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

1303 lines
44 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">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light dark">
<title>LandveX Admin — Infrastrukturportal</title>
<link rel="stylesheet" >
<style>
/* ═══════════════════════════════════════════
LANDVEX ADMIN PORTAL — iOS Native
Wavult Group · 2026
═══════════════════════════════════════════ */
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-font-smoothing: antialiased; }
body {
font-family: var(--font-text);
background: var(--background-secondary);
color: var(--label-primary);
min-height: 100vh;
}
/* ── NAVIGATION BAR ── */
.nav-bar {
position: sticky;
top: 0;
z-index: 100;
height: 44px;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-bottom: 0.5px solid var(--separator);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
safe-area-inset-top: env(safe-area-inset-top);
}
.nav-title {
font-family: var(--font-display);
font-size: 17px;
font-weight: 600;
letter-spacing: -0.4px;
color: var(--label-primary);
}
.nav-logo {
display: flex;
align-items: center;
gap: 8px;
}
.nav-logo .logo-icon {
width: 28px;
height: 28px;
background: linear-gradient(135deg, #007AFF 0%, #0055CC 100%);
border-radius: 7px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: white;
font-weight: 700;
letter-spacing: -0.5px;
}
.badge-admin {
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
background: rgba(255, 149, 0, 0.15);
color: var(--ios-orange);
border-radius: 20px;
letter-spacing: 0.3px;
}
/* ── CONTENT ── */
.content {
padding: 0 16px 120px;
max-width: 1100px;
margin: 0 auto;
}
/* ── SECTION HEADER ── */
.section-header {
padding: 20px 0 8px;
font-family: var(--font-display);
font-size: 20px;
font-weight: 700;
letter-spacing: -0.4px;
color: var(--label-primary);
}
.section-header-large {
padding: 28px 0 4px;
font-family: var(--font-display);
font-size: 28px;
font-weight: 700;
letter-spacing: -0.5px;
}
.section-sub {
font-size: 15px;
color: var(--label-secondary);
padding-bottom: 12px;
}
/* ── METRICS GRID ── */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 8px;
}
.metric-card {
background: var(--background-primary);
border-radius: 16px;
padding: 16px;
border: 0.5px solid var(--separator);
}
.metric-value {
font-family: var(--font-display);
font-size: 32px;
font-weight: 700;
letter-spacing: -1px;
line-height: 1;
margin-bottom: 4px;
}
.metric-label {
font-size: 13px;
color: var(--label-secondary);
font-weight: 500;
}
.metric-trend {
font-size: 12px;
font-weight: 600;
margin-top: 6px;
}
.trend-up { color: var(--ios-green); }
.trend-down { color: var(--ios-red); }
.trend-flat { color: var(--label-tertiary); }
.metric-blue .metric-value { color: var(--ios-blue); }
.metric-green .metric-value { color: var(--ios-green); }
.metric-orange .metric-value { color: var(--ios-orange); }
.metric-red .metric-value { color: var(--ios-red); }
/* ── TABLE ── */
.table-card {
background: var(--background-primary);
border-radius: 16px;
border: 0.5px solid var(--separator);
overflow: hidden;
margin-bottom: 12px;
}
.table-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 0.5px solid var(--separator);
}
.table-title {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.3px;
}
.table-action {
font-size: 15px;
color: var(--ios-blue);
font-weight: 500;
cursor: pointer;
background: none;
border: none;
padding: 0;
}
.table-scroll {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
thead th {
text-align: left;
padding: 8px 16px;
font-size: 12px;
font-weight: 600;
color: var(--label-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
background: var(--background-secondary);
border-bottom: 0.5px solid var(--separator);
}
tbody tr {
border-bottom: 0.5px solid var(--separator);
transition: background 0.15s;
cursor: pointer;
}
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: var(--fill-quaternary); }
tbody td {
padding: 12px 16px;
vertical-align: middle;
}
.td-name {
font-weight: 600;
letter-spacing: -0.2px;
}
.td-sub {
font-size: 12px;
color: var(--label-secondary);
margin-top: 2px;
}
/* ── STATUS BADGES ── */
.status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 20px;
}
.status-ok {
background: rgba(52, 199, 89, 0.15);
color: var(--ios-green);
}
.status-warn {
background: rgba(255, 149, 0, 0.15);
color: var(--ios-orange);
}
.status-critical {
background: rgba(255, 59, 48, 0.15);
color: var(--ios-red);
}
.status-pending {
background: rgba(142, 142, 147, 0.15);
color: var(--ios-gray);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
/* ── RISK SCORE ── */
.risk-bar-wrap {
display: flex;
align-items: center;
gap: 8px;
}
.risk-bar {
flex: 1;
height: 4px;
background: var(--fill-tertiary);
border-radius: 2px;
overflow: hidden;
min-width: 60px;
}
.risk-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
.risk-low .risk-fill { background: var(--ios-green); }
.risk-medium .risk-fill { background: var(--ios-orange); }
.risk-high .risk-fill { background: var(--ios-red); }
.risk-label {
font-size: 12px;
font-weight: 600;
min-width: 28px;
text-align: right;
}
/* ── MAP PLACEHOLDER ── */
.map-card {
background: var(--background-primary);
border-radius: 16px;
border: 0.5px solid var(--separator);
overflow: hidden;
margin-bottom: 12px;
}
.map-container {
height: 320px;
background: #E8EDF2;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
@media (prefers-color-scheme: dark) {
.map-container { background: #1C2328; }
}
.map-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 8px;
color: var(--label-secondary);
}
.map-grid {
position: absolute;
inset: 0;
opacity: 0.3;
background-image:
linear-gradient(rgba(0,122,255,0.3) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,122,255,0.3) 1px, transparent 1px);
background-size: 40px 40px;
}
.map-pin {
position: absolute;
width: 28px;
height: 28px;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
transition: transform 0.2s;
}
.map-pin:hover { transform: rotate(-45deg) scale(1.2); }
.map-pin-ok { background: var(--ios-green); }
.map-pin-warn { background: var(--ios-orange); }
.map-pin-critical { background: var(--ios-red); }
.map-pin-inner {
transform: rotate(45deg);
font-size: 11px;
color: white;
}
.map-legend {
position: absolute;
bottom: 12px;
right: 12px;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-radius: 12px;
padding: 8px 12px;
border: 0.5px solid var(--separator);
display: flex;
gap: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--label-secondary);
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
/* ── FAB BUTTON ── */
.fab {
position: fixed;
bottom: calc(24px + env(safe-area-inset-bottom));
right: 20px;
z-index: 200;
background: var(--ios-blue);
color: white;
border: none;
border-radius: 28px;
height: 56px;
padding: 0 20px;
font-size: 15px;
font-weight: 600;
font-family: var(--font-text);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 20px rgba(0, 122, 255, 0.4);
transition: transform 0.15s, box-shadow 0.15s;
}
.fab:hover {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(0, 122, 255, 0.5);
}
.fab:active {
transform: translateY(0) scale(0.97);
box-shadow: 0 2px 12px rgba(0, 122, 255, 0.35);
}
/* ── INSPECTION CELLS ── */
.inspection-list {
background: var(--background-primary);
border-radius: 16px;
border: 0.5px solid var(--separator);
overflow: hidden;
}
.inspection-item {
display: flex;
align-items: center;
padding: 14px 16px;
border-bottom: 0.5px solid var(--separator);
gap: 12px;
cursor: pointer;
transition: background 0.15s;
}
.inspection-item:last-child { border-bottom: none; }
.inspection-item:hover { background: var(--fill-quaternary); }
.inspection-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.inspection-body { flex: 1; min-width: 0; }
.inspection-title {
font-size: 15px;
font-weight: 600;
letter-spacing: -0.2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.inspection-meta {
font-size: 13px;
color: var(--label-secondary);
margin-top: 2px;
}
.inspection-chevron {
font-size: 17px;
color: var(--label-tertiary);
flex-shrink: 0;
}
/* ── MODAL ── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(242,242,247,0.4);
backdrop-filter: blur(4px);
z-index: 300;
display: flex;
align-items: flex-end;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
}
.modal-overlay.open {
opacity: 1;
pointer-events: all;
}
.modal-sheet {
background: var(--background-primary);
border-radius: 20px 20px 0 0;
width: 100%;
max-width: 640px;
padding: 8px 0 env(safe-area-inset-bottom);
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}
.modal-overlay.open .modal-sheet {
transform: translateY(0);
}
.modal-handle {
width: 36px;
height: 4px;
background: var(--fill-primary);
border-radius: 2px;
margin: 8px auto 16px;
}
.modal-title {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.4px;
padding: 0 20px 16px;
border-bottom: 0.5px solid var(--separator);
}
.form-group {
padding: 16px 20px;
border-bottom: 0.5px solid var(--separator);
}
.form-label {
font-size: 13px;
font-weight: 600;
color: var(--label-secondary);
margin-bottom: 6px;
}
.form-input {
width: 100%;
padding: 10px 12px;
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 10px;
font-size: 15px;
font-family: var(--font-text);
color: var(--label-primary);
outline: none;
transition: border-color 0.2s;
}
.form-input:focus {
border-color: var(--ios-blue);
}
.form-select {
width: 100%;
padding: 10px 12px;
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 10px;
font-size: 15px;
font-family: var(--font-text);
color: var(--label-primary);
outline: none;
appearance: none;
cursor: pointer;
}
.modal-actions {
padding: 16px 20px;
display: flex;
gap: 10px;
}
.btn-primary {
flex: 1;
height: 50px;
background: var(--ios-blue);
color: white;
border: none;
border-radius: 14px;
font-size: 17px;
font-weight: 600;
font-family: var(--font-text);
cursor: pointer;
transition: opacity 0.15s;
}
.btn-primary:active { opacity: 0.7; }
.btn-secondary {
flex: 1;
height: 50px;
background: var(--fill-secondary);
color: var(--label-primary);
border: none;
border-radius: 14px;
font-size: 17px;
font-weight: 600;
font-family: var(--font-text);
cursor: pointer;
transition: opacity 0.15s;
}
.btn-secondary:active { opacity: 0.7; }
/* ── TAB BAR ── */
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: calc(49px + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-top: 0.5px solid var(--separator);
display: flex;
align-items: flex-start;
justify-content: space-around;
padding-top: 8px;
z-index: 90;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
cursor: pointer;
flex: 1;
padding: 0 8px;
opacity: 0.6;
transition: opacity 0.15s;
}
.tab-item.active { opacity: 1; }
.tab-icon { font-size: 22px; }
.tab-label { font-size: 10px; font-weight: 500; }
.tab-item.active .tab-label { color: var(--ios-blue); }
.tab-item.active .tab-icon { filter: none; }
/* ── DARK MODE ── */
@media (prefers-color-scheme: dark) {
.map-container { background: #1A1E23; }
}
/* ── RESPONSIVE ── */
@media (min-width: 768px) {
.metrics-grid { grid-template-columns: repeat(4, 1fr); }
.content { padding: 0 24px 80px; }
}
@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(0,0,0,0.4)"] { color: rgba(0,0,0,0.72) !important; }
[style*="rgba(0,0,0,0.3)"] { color: rgba(0,0,0,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>
</head>
<body>
<!-- NAV BAR -->
<nav class="nav-bar">
<div class="nav-logo">
<div class="logo-icon">LX</div>
<span class="nav-title">LandveX Admin</span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<span class="badge-admin">ADMIN</span>
<div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#007AFF,#0055CC);display:flex;align-items:center;justify-content:center;color:white;font-size:14px;font-weight:700;">A</div>
</div>
</nav>
<!-- MAIN CONTENT -->
<main class="content">
<!-- HEADER -->
<div class="section-header-large">Infrastrukturportal</div>
<div class="section-sub">LandveX · Administration · 18 maj 2026</div>
<!-- METRICS -->
<div class="section-header">Systemöversikt</div>
<div class="metrics-grid">
<div class="metric-card metric-blue">
<div class="metric-value">247</div>
<div class="metric-label">Registrerade objekt</div>
<div class="metric-trend trend-up">↑ 12 denna vecka</div>
</div>
<div class="metric-card metric-green">
<div class="metric-value">189</div>
<div class="metric-label">OK-status</div>
<div class="metric-trend trend-up">↑ 76%</div>
</div>
<div class="metric-card metric-orange">
<div class="metric-value">41</div>
<div class="metric-label">Kräver åtgärd</div>
<div class="metric-trend trend-flat">↔ Stabil</div>
</div>
<div class="metric-card metric-red">
<div class="metric-value">17</div>
<div class="metric-label">Kritiska</div>
<div class="metric-trend trend-down">↑ 3 sedan igår</div>
</div>
</div>
<!-- MAP -->
<div class="section-header">Live Operationell Karta</div>
<!-- LIVE MAP WITH ZOOMER POSITIONS -->
<div class="map-card" style="margin-bottom:12px;">
<div class="table-header">
<span class="table-title">📍 Realtids zoomers + objekt</span>
<button class="table-action" onclick="cycleZoomerPos()">↻ Uppdatera</button>
</div>
<div class="map-container" id="map" style="position:relative;min-height:260px;">
<div class="map-grid"></div>
<!-- Object pins -->
<div class="map-pin map-pin-ok" style="left:22%;top:30%;" title="Gamla Stan Bron · OK"><div class="map-pin-inner"></div></div>
<div class="map-pin map-pin-warn" style="left:45%;top:45%;" title="E4 Avsnitt 12 · Varning"><div class="map-pin-inner">!</div></div>
<div class="map-pin map-pin-critical" style="left:68%;top:25%;" title="E4 Älvsjö · Kritisk"><div class="map-pin-inner"></div></div>
<div class="map-pin map-pin-ok" style="left:35%;top:65%;" title="Arlanda Terminal 5 · OK"><div class="map-pin-inner"></div></div>
<div class="map-pin map-pin-warn" style="left:80%;top:55%;" title="Öresundsbron · Varning"><div class="map-pin-inner">!</div></div>
<div class="map-pin map-pin-ok" style="left:55%;top:72%;" title="Göta Älvbron · OK"><div class="map-pin-inner"></div></div>
<!-- Live zoomer pins -->
<div id="zoomer-lx01" style="position:absolute;left:48%;top:42%;z-index:10;transition:all 2s ease;">
<div style="background:var(--ios-blue);color: #1d1d1f;border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 2px 8px rgba(0,122,255,0.5);animation:zoomer-pulse 2s infinite;">🚁</div>
<div style="font-size:9px;font-weight:700;color:var(--ios-blue);text-align:center;margin-top:2px;background:rgba(0,0,0,0.9);border-radius:6px;padding:1px 4px;">LX-01</div>
</div>
<div id="zoomer-lx02" style="position:absolute;left:24%;top:27%;z-index:10;transition:all 2s ease;">
<div style="background:var(--ios-green);color: #1d1d1f;border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 2px 8px rgba(52,199,89,0.5);">🚁</div>
<div style="font-size:9px;font-weight:700;color:var(--ios-green);text-align:center;margin-top:2px;background:rgba(0,0,0,0.9);border-radius:6px;padding:1px 4px;">LX-02</div>
</div>
<div id="zoomer-lx03" style="position:absolute;left:72%;top:58%;z-index:10;transition:all 2s ease;">
<div style="background:var(--ios-orange);color: #1d1d1f;border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 2px 8px rgba(255,149,0,0.5);">🚁</div>
<div style="font-size:9px;font-weight:700;color:var(--ios-orange);text-align:center;margin-top:2px;background:rgba(0,0,0,0.9);border-radius:6px;padding:1px 4px;">LX-03 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
</div>
<!-- Live indicator -->
<div style="position:absolute;top:10px;left:10px;z-index:11;display:flex;align-items:center;gap:5px;background:rgba(0,0,0,0.9);border-radius:20px;padding:4px 10px;font-size:11px;font-weight:600;color:var(--ios-green);">
<div style="width:6px;height:6px;border-radius:50%;background:var(--ios-green);animation:zoomer-pulse 2s infinite;"></div>
LIVE
</div>
<div class="map-legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--ios-green)"></div>OK (189)</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--ios-orange)"></div>Varning (41)</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--ios-red)"></div>Kritisk (17)</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--ios-blue)"></div>🚁 Zoomer (3)</div>
</div>
</div>
</div>
<!-- UPPDRAGSSTATUS -->
<div class="section-header">Uppdragsstatus</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:12px;">
<div class="metric-card metric-green">
<div class="metric-value">14</div>
<div class="metric-label">Pågående</div>
<div class="metric-trend trend-up">🚁 Aktiva uppdrag</div>
</div>
<div class="metric-card metric-blue">
<div class="metric-value">218</div>
<div class="metric-label">Klart idag</div>
<div class="metric-trend trend-up">↑ 23% vs plan</div>
</div>
<div class="metric-card metric-orange">
<div class="metric-value">3</div>
<div class="metric-label">Avvikelse</div>
<div class="metric-trend trend-down">↓ Kräver åtgärd</div>
</div>
</div>
<!-- ACTIVE MISSIONS TABLE -->
<div class="table-card" style="margin-bottom:12px;">
<div class="table-header">
<span class="table-title">⚡ Pågående uppdrag</span>
<button class="table-action" onclick="openDispatch()">Snabb-dispatch →</button>
</div>
<div class="table-scroll">
<table>
<thead><tr>
<th>Zoomer</th><th>Objekt</th><th>Status</th><th>Progress</th><th>ETA</th><th>Åtgärd</th>
</tr></thead>
<tbody>
<tr>
<td><span style="font-weight:600;color:var(--ios-blue);">🚁 LX-01</span></td>
<td>E4 Avsnitt 12</td>
<td><span style="background:rgba(52,199,89,0.12);color:var(--ios-green);font-size:11px;font-weight:700;padding:3px 8px;border-radius:20px;">PÅGÅR</span></td>
<td>
<div style="width:80px;height:5px;background:var(--fill-tertiary);border-radius:3px;overflow:hidden;">
<div style="width:72%;height:100%;background:var(--ios-blue);border-radius:3px;"></div>
</div>
<div style="font-size:10px;color:var(--label-tertiary);margin-top:2px;">72%</div>
</td>
<td style="font-size:13px;">14 min</td>
<td><button style="font-size:12px;color:var(--ios-blue);background:none;border:none;cursor:pointer;padding:4px 0;">Följ live</button></td>
</tr>
<tr>
<td><span style="font-weight:600;color:var(--ios-green);">🚁 LX-02</span></td>
<td>Gamla Stan Bron</td>
<td><span style="background:rgba(52,199,89,0.12);color:var(--ios-green);font-size:11px;font-weight:700;padding:3px 8px;border-radius:20px;">PÅGÅR</span></td>
<td>
<div style="width:80px;height:5px;background:var(--fill-tertiary);border-radius:3px;overflow:hidden;">
<div style="width:35%;height:100%;background:var(--ios-green);border-radius:3px;"></div>
</div>
<div style="font-size:10px;color:var(--label-tertiary);margin-top:2px;">35%</div>
</td>
<td style="font-size:13px;">38 min</td>
<td><button style="font-size:12px;color:var(--ios-blue);background:none;border:none;cursor:pointer;padding:4px 0;">Följ live</button></td>
</tr>
<tr>
<td><span style="font-weight:600;color:var(--ios-orange);">🚁 LX-03</span></td>
<td>Öresundsbron S3</td>
<td><span style="background:rgba(255,149,0,0.12);color:var(--ios-orange);font-size:11px;font-weight:700;padding:3px 8px;border-radius:20px;">AVVIKELSE</span></td>
<td>
<div style="width:80px;height:5px;background:var(--fill-tertiary);border-radius:3px;overflow:hidden;">
<div style="width:58%;height:100%;background:var(--ios-orange);border-radius:3px;"></div>
</div>
<div style="font-size:10px;color:var(--label-tertiary);margin-top:2px;">58%</div>
</td>
<td style="font-size:13px;">Försenad</td>
<td><button style="font-size:12px;color:var(--ios-red);background:none;border:none;cursor:pointer;font-weight:600;padding:4px 0;" onclick="openDispatch()">Dispatch ny</button></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- KOSTNADSEFFEKTIVITET -->
<div class="section-header">Kostnadseffektivitet</div>
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:12px;">
<div class="metric-card metric-green">
<div class="metric-value">1 240</div>
<div class="metric-label">Pris/inspektion (SEK)</div>
<div class="metric-trend trend-up">↓ 62% vs marknad</div>
</div>
<div class="metric-card metric-blue">
<div class="metric-value">3 280</div>
<div class="metric-label">Marknadspris (SEK)</div>
<div class="metric-trend flat">→ Traditionell inspektion</div>
</div>
<div class="metric-card metric-green">
<div class="metric-value">218</div>
<div class="metric-label">Insp. idag</div>
<div class="metric-trend trend-up">Rekord <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0066FF" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg></div>
</div>
<div class="metric-card metric-orange">
<div class="metric-value">270k</div>
<div class="metric-label">Besparing idag (SEK)</div>
<div class="metric-trend trend-up">↑ vs traditionellt</div>
</div>
</div>
<!-- PIPELINE -->
<div class="section-header">Inkommande beställningar</div>
<div class="table-card" style="margin-bottom:12px;">
<div class="table-header">
<span class="table-title">Pipeline · 7 väntande</span>
<button class="table-action">Hantera alla →</button>
</div>
<div class="table-scroll">
<table>
<thead><tr>
<th>Kund</th><th>Objekt</th><th>Typ</th><th>Prio</th><th>Mottagen</th><th>Åtgärd</th>
</tr></thead>
<tbody>
<tr>
<td style="font-weight:600;">Trafikverket</td><td>E4 Älvsjö · Kritisk</td><td>Fullständig</td>
<td><span style="background:rgba(255,59,48,0.12);color:var(--ios-red);font-size:11px;font-weight:700;padding:2px 7px;border-radius:20px;">KRITISK</span></td>
<td style="font-size:12px;color:var(--label-tertiary);">09:12</td>
<td><button style="font-size:12px;color: #1d1d1f;background:var(--ios-red);border:none;border-radius:12px;padding:4px 12px;cursor:pointer;font-weight:600;" onclick="openDispatch()">Dispatch nu</button></td>
</tr>
<tr>
<td style="font-weight:600;">Region Stockholm</td><td>Gamla Stan Bro</td><td>Ytinspektion</td>
<td><span style="background:rgba(255,149,0,0.12);color:var(--ios-orange);font-size:11px;font-weight:700;padding:2px 7px;border-radius:20px;">HÖG</span></td>
<td style="font-size:12px;color:var(--label-tertiary);">08:45</td>
<td><button style="font-size:12px;color: #1d1d1f;background:var(--ios-orange);border:none;border-radius:12px;padding:4px 12px;cursor:pointer;font-weight:600;" onclick="openDispatch()">Dispatch</button></td>
</tr>
<tr>
<td style="font-weight:600;">NCC Sverige</td><td>Rv 40 Borås</td><td>Strukturkontroll</td>
<td><span style="background:rgba(0,122,255,0.10);color:var(--ios-blue);font-size:11px;font-weight:700;padding:2px 7px;border-radius:20px;">NORMAL</span></td>
<td style="font-size:12px;color:var(--label-tertiary);">Igår</td>
<td><button style="font-size:12px;color:var(--ios-blue);background:none;border:1px solid var(--ios-blue);border-radius:12px;padding:4px 12px;cursor:pointer;font-weight:600;">Schemalägg</button></td>
</tr>
<tr>
<td style="font-weight:600;">Ellevio AB</td><td>Kraftledning N32</td><td>Optisk scanning</td>
<td><span style="background:rgba(0,122,255,0.10);color:var(--ios-blue);font-size:11px;font-weight:700;padding:2px 7px;border-radius:20px;">NORMAL</span></td>
<td style="font-size:12px;color:var(--label-tertiary);">Igår</td>
<td><button style="font-size:12px;color:var(--ios-blue);background:none;border:1px solid var(--ios-blue);border-radius:12px;padding:4px 12px;cursor:pointer;font-weight:600;">Schemalägg</button></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- KARTVY (original) -->
<div class="section-header" style="display:none;">Kartvy (Legacy)</div>
<!-- OBJECTS TABLE -->
<div class="section-header">Registrerade objekt</div>
<div class="table-card">
<div class="table-header">
<span class="table-title">Fastigheter & Infrastruktur</span>
<button class="table-action" onclick="openModal()">+ Nytt objekt</button>
</div>
<div class="table-scroll">
<table>
<thead>
<tr>
<th>Objekt</th>
<th>Typ</th>
<th>Status</th>
<th>Risk</th>
<th>Senaste inspektion</th>
<th>Nästa inspektion</th>
</tr>
</thead>
<tbody>
<tr onclick="alert('Öppnar objektdetaljer: E4 Norrtull')">
<td><div class="td-name">E4 Norrtull Avsnitt 12</div><div class="td-sub">REF-001 · Stockholm</div></td>
<td>🛣️ Väg</td>
<td><span class="status-badge status-warn"><span class="status-dot"></span>Kräver åtgärd</span></td>
<td>
<div class="risk-bar-wrap risk-medium">
<div class="risk-bar"><div class="risk-fill" style="width:58%"></div></div>
<span class="risk-label">58</span>
</div>
</td>
<td>2026-05-10</td>
<td>2026-05-25</td>
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Gamla Stan')">
<td><div class="td-name">Gamla Stan Bro</div><div class="td-sub">REF-002 · Stockholm</div></td>
<td>🌉 Bro</td>
<td><span class="status-badge status-ok"><span class="status-dot"></span>OK</span></td>
<td>
<div class="risk-bar-wrap risk-low">
<div class="risk-bar"><div class="risk-fill" style="width:22%"></div></div>
<span class="risk-label">22</span>
</div>
</td>
<td>2026-05-14</td>
<td>2026-08-14</td>
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Kungliga Slottet')">
<td><div class="td-name">Kungliga Slottet Tak</div><div class="td-sub">REF-003 · Stockholm</div></td>
<td>🏛️ Byggnad</td>
<td><span class="status-badge status-critical"><span class="status-dot"></span>Kritisk</span></td>
<td>
<div class="risk-bar-wrap risk-high">
<div class="risk-bar"><div class="risk-fill" style="width:87%"></div></div>
<span class="risk-label">87</span>
</div>
</td>
<td>2026-05-01</td>
<td>🔴 Omedelbart</td>
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Arlanda Terminal 5')">
<td><div class="td-name">Arlanda Terminal 5</div><div class="td-sub">REF-004 · Sigtuna</div></td>
<td>✈️ Byggnad</td>
<td><span class="status-badge status-ok"><span class="status-dot"></span>OK</span></td>
<td>
<div class="risk-bar-wrap risk-low">
<div class="risk-bar"><div class="risk-fill" style="width:15%"></div></div>
<span class="risk-label">15</span>
</div>
</td>
<td>2026-05-16</td>
<td>2026-11-16</td>
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Öresundsbron')">
<td><div class="td-name">Öresundsbron Sektion 3</div><div class="td-sub">REF-005 · Malmö</div></td>
<td>🌉 Bro</td>
<td><span class="status-badge status-warn"><span class="status-dot"></span>Kräver åtgärd</span></td>
<td>
<div class="risk-bar-wrap risk-medium">
<div class="risk-bar"><div class="risk-fill" style="width:63%"></div></div>
<span class="risk-label">63</span>
</div>
</td>
<td>2026-04-28</td>
<td>2026-05-20</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- INSPECTIONS -->
<div class="section-header">Senaste inspektioner</div>
<div class="inspection-list">
<div class="inspection-item">
<div class="inspection-icon" style="background:rgba(52,199,89,0.15)">🏗️</div>
<div class="inspection-body">
<div class="inspection-title">E4 Norrtull — Ytinspektion</div>
<div class="inspection-meta">Anna Lindqvist · 2026-05-10 · <span style="color:var(--ios-green)">Godkänd</span></div>
</div>
<div class="inspection-chevron"></div>
</div>
<div class="inspection-item">
<div class="inspection-icon" style="background:rgba(255,149,0,0.15)">🌉</div>
<div class="inspection-body">
<div class="inspection-title">Öresundsbron — Strukturkontroll</div>
<div class="inspection-meta">Marcus Holm · 2026-04-28 · <span style="color:var(--ios-orange)">Åtgärder krävs</span></div>
</div>
<div class="inspection-chevron"></div>
</div>
<div class="inspection-item">
<div class="inspection-icon" style="background:rgba(255,59,48,0.15)">🏛️</div>
<div class="inspection-body">
<div class="inspection-title">Kungliga Slottet — Takinspektion</div>
<div class="inspection-meta">Sofia Berg · 2026-05-01 · <span style="color:var(--ios-red)">Kritisk rapport</span></div>
</div>
<div class="inspection-chevron"></div>
</div>
<div class="inspection-item">
<div class="inspection-icon" style="background:rgba(0,122,255,0.15)">✈️</div>
<div class="inspection-body">
<div class="inspection-title">Arlanda T5 — Fasadinspektion</div>
<div class="inspection-meta">Erik Svensson · 2026-05-16 · <span style="color:var(--ios-green)">Godkänd</span></div>
</div>
<div class="inspection-chevron"></div>
</div>
</div>
<!-- STATUS REPORTS -->
<div class="section-header">Statusrapporter</div>
<div class="table-card">
<div class="table-scroll">
<table>
<thead>
<tr>
<th>Rapport</th>
<th>Period</th>
<th>Objekt</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><div class="td-name">Månatlig Statusrapport</div><div class="td-sub">Automatisk</div></td>
<td>Maj 2026</td>
<td>247 objekt</td>
<td><span class="status-badge status-ok"><span class="status-dot"></span>Klar</span></td>
<td><button class="table-action">Ladda ned</button></td>
</tr>
<tr>
<td><div class="td-name">Kritiska avvikelser</div><div class="td-sub">Automatisk · Alert</div></td>
<td>2026-05-17</td>
<td>3 objekt</td>
<td><span class="status-badge status-critical"><span class="status-dot"></span>Åtgärda</span></td>
<td><button class="table-action">Visa</button></td>
</tr>
<tr>
<td><div class="td-name">Kvartal Q1 2026</div><div class="td-sub">Manuell</div></td>
<td>JanMar 2026</td>
<td>231 objekt</td>
<td><span class="status-badge status-ok"><span class="status-dot"></span>Klar</span></td>
<td><button class="table-action">Ladda ned</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
<!-- FAB -->
<button class="fab" onclick="openModal()">
<span>+</span>
<span>Ny inspektion</span>
</button>
<!-- MODAL: Ny inspektion -->
<div class="modal-overlay" id="modal" onclick="closeModal(event)">
<div class="modal-sheet" onclick="event.stopPropagation()">
<div class="modal-handle"></div>
<div class="modal-title">Skapa ny inspektion</div>
<div class="form-group">
<div class="form-label">Objekt</div>
<select class="form-select">
<option>E4 Norrtull Avsnitt 12</option>
<option>Gamla Stan Bro</option>
<option>Kungliga Slottet Tak</option>
<option>Arlanda Terminal 5</option>
<option>Öresundsbron Sektion 3</option>
</select>
</div>
<div class="form-group">
<div class="form-label">Inspektionstyp</div>
<select class="form-select">
<option>Ytinspektion</option>
<option>Strukturkontroll</option>
<option>Fasadinspektion</option>
<option>Takinspektion</option>
<option>Fullständig inspektion</option>
</select>
</div>
<div class="form-group">
<div class="form-label">Ansvarig inspektion</div>
<input class="form-input" type="text" placeholder="Sök inspektör...">
</div>
<div class="form-group">
<div class="form-label">Datum</div>
<input class="form-input" type="date" value="2026-05-20">
</div>
<div class="modal-actions">
<button class="btn-secondary" onclick="closeModal()">Avbryt</button>
<button class="btn-primary" onclick="submitInspection()">Skapa inspektion</button>
</div>
</div>
</div>
<!-- TAB BAR -->
<nav class="tab-bar">
<div class="tab-item active">
<div class="tab-icon">🏛️</div>
<div class="tab-label">Översikt</div>
</div>
<div class="tab-item" onclick="window.location.href='landvex-admin.html#objects'">
<div class="tab-icon">📋</div>
<div class="tab-label">Objekt</div>
</div>
<div class="tab-item" onclick="window.location.href='landvex-admin.html#inspections'">
<div class="tab-icon">🔍</div>
<div class="tab-label">Inspektioner</div>
</div>
<div class="tab-item">
<div class="tab-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0066FF" stroke-width="2"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg></div>
<div class="tab-label">Rapporter</div>
</div>
<div class="tab-item">
<div class="tab-icon">⚙️</div>
<div class="tab-label">Inställningar</div>
</div>
</nav>
<style>
@keyframes zoomer-pulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0,122,255,0.4); }
50% { transform: scale(1.05); box-shadow: 0 0 0 6px rgba(0,122,255,0); }
}
@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>
<!-- DISPATCH MODAL -->
<div id="dispatch-modal" style="position:fixed;inset:0;background: rgba(242,242,247,0.5);display:flex;align-items:flex-end;z-index:1001;opacity:0;pointer-events:none;transition:opacity 0.25s;">
<div style="background:var(--background-primary);border-radius:20px 20px 0 0;padding:20px 20px 40px;width:100%;max-height:80vh;overflow-y:auto;transform:translateY(100%);transition:transform 0.35s cubic-bezier(0.34,1.56,0.64,1);" id="dispatch-sheet">
<div style="width:36px;height:4px;background:var(--fill-secondary);border-radius:2px;margin:0 auto 16px;"></div>
<div style="font-family:var(--font-display);font-size:22px;font-weight:700;letter-spacing:-0.4px;margin-bottom:16px;">⚡ Snabb-dispatch</div>
<div style="background:var(--background-secondary);border-radius:12px;padding:14px;margin-bottom:12px;">
<div style="font-size:13px;font-weight:600;color:var(--label-secondary);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:10px;">Närmaste lediga zoomer</div>
<div style="display:flex;align-items:center;gap:12px;padding:10px;background:rgba(52,199,89,0.08);border-radius:10px;border:0.5px solid rgba(52,199,89,0.3);margin-bottom:8px;">
<span style="font-size:24px;">🚁</span>
<div style="flex:1;">
<div style="font-size:16px;font-weight:600;">LX-04 · Stockholm N</div>
<div style="font-size:12px;color:var(--label-secondary);">4.2 km bort · Laddad 100% · HD + Multispektral</div>
</div>
<button onclick="confirmDispatch('LX-04')" style="background:var(--ios-green);color: #1d1d1f;border:none;border-radius:12px;padding:8px 14px;font-size:14px;font-weight:600;cursor:pointer;">Skicka</button>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:10px;background:var(--fill-quaternary);border-radius:10px;margin-bottom:8px;">
<span style="font-size:24px;">🚁</span>
<div style="flex:1;">
<div style="font-size:16px;font-weight:600;">LX-07 · Nacka</div>
<div style="font-size:12px;color:var(--label-secondary);">8.7 km bort · Laddad 87% · HD</div>
</div>
<button onclick="confirmDispatch('LX-07')" style="background:var(--ios-blue);color: #1d1d1f;border:none;border-radius:12px;padding:8px 14px;font-size:14px;font-weight:600;cursor:pointer;">Skicka</button>
</div>
</div>
<div id="dispatch-success" style="display:none;text-align:center;padding:16px;background:rgba(52,199,89,0.1);border-radius:14px;color:var(--ios-green);font-size:16px;font-weight:600;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#00C853" stroke-width="3"><path d="M20 6L9 17l-5-5"/></svg> Dispatch bekräftad! Zoomer anländer inom 12 min.</div>
<button onclick="closeDispatch()" style="width:100%;padding:14px;background:var(--fill-tertiary);color:var(--label-primary);border:none;border-radius:14px;font-size:17px;font-weight:600;cursor:pointer;margin-top:8px;">Stäng</button>
</div>
</div>
<script>
function openModal() {
document.getElementById('modal').classList.add('open');
}
function closeModal(e) {
if (!e || e.target === document.getElementById('modal')) {
document.getElementById('modal').classList.remove('open');
}
}
function submitInspection() {
document.getElementById('modal').classList.remove('open');
// Toast feedback
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:var(--ios-green);color:white;padding:12px 20px;border-radius:20px;font-size:15px;font-weight:600;z-index:500;box-shadow:0 4px 16px rgba(52,199,89,0.4);';
toast.textContent = '✓ Inspektion skapad';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
}
// Tab switching
document.querySelectorAll('.tab-item').forEach((tab, i) => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
// Dispatch modal
function openDispatch() {
const m = document.getElementById('dispatch-modal');
const s = document.getElementById('dispatch-sheet');
document.getElementById('dispatch-success').style.display = 'none';
m.style.opacity = '1';
m.style.pointerEvents = 'all';
setTimeout(() => s.style.transform = 'translateY(0)', 10);
}
function closeDispatch() {
const m = document.getElementById('dispatch-modal');
const s = document.getElementById('dispatch-sheet');
s.style.transform = 'translateY(100%)';
setTimeout(() => { m.style.opacity = '0'; m.style.pointerEvents = 'none'; }, 300);
}
function confirmDispatch(zoomerId) {
setTimeout(() => {
document.getElementById('dispatch-success').style.display = 'block';
}, 800);
}
document.getElementById('dispatch-modal').addEventListener('click', (e) => {
if (e.target === document.getElementById('dispatch-modal')) closeDispatch();
});
// Zoomer live position animation
function cycleZoomerPos() {
const z1 = document.getElementById('zoomer-lx01');
const z2 = document.getElementById('zoomer-lx02');
const z3 = document.getElementById('zoomer-lx03');
if (z1) z1.style.left = (44 + Math.random() * 8) + '%';
if (z1) z1.style.top = (38 + Math.random() * 8) + '%';
if (z2) z2.style.left = (20 + Math.random() * 8) + '%';
if (z2) z2.style.top = (25 + Math.random() * 8) + '%';
if (z3) z3.style.left = (68 + Math.random() * 8) + '%';
if (z3) z3.style.top = (52 + Math.random() * 8) + '%';
}
// Auto-animate zoomers every 5s
setInterval(cycleZoomerPos, 5000);
</script>
</body>
</html>