Files
boc/command-center/index.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

362 lines
53 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.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#001E50">
<title>AAMOS Command Center — Landvex Operations</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--navy:#001E50;--amber:#C8763A;--bg:#F5F5F0;--card:#fff;--muted:#6B6B70;--green:#34C759;--red:#FF3B30;--yellow:#FFCC00;--border:#E8E3DC;--radius:14px;--font:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif}
html{font-family:var(--font);font-size:16px;background:var(--bg);color:var(--navy)}
body{min-height:100vh;padding-bottom:80px}
#header{background:var(--navy);color:#fff;padding:16px 20px;position:sticky;top:0;z-index:100;display:flex;align-items:center;gap:12px}
#header .logo{width:36px;height:36px;border-radius:10px;background:var(--amber);display:flex;align-items:center;justify-content:center;font-size:18px}
#header h1{font-size:1.1rem;font-weight:700;flex:1}
#header .status{font-size:.75rem;opacity:.7}
#nav{display:flex;gap:8px;padding:12px 16px;overflow-x:auto;background:#fff;border-bottom:1px solid var(--border);-webkit-overflow-scrolling:touch;scrollbar-width:none}
#nav::-webkit-scrollbar{display:none}
#nav button{white-space:nowrap;padding:8px 16px;border-radius:20px;border:1px solid var(--border);background:#fff;color:var(--navy);font-size:.88rem;font-weight:500;cursor:pointer}
#nav button.active{background:var(--navy);color:#fff;border-color:var(--navy)}
.section{padding:16px;display:none}
.section.active{display:block}
.card{background:var(--card);border-radius:var(--radius);padding:16px;margin-bottom:12px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
.card h3{font-size:.85rem;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:12px}
.card h3 .live{color:var(--green);font-size:.7rem;margin-left:6px;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
.kpi-row{display:flex;gap:12px;overflow-x:auto}
.kpi{min-width:140px;flex:1;background:var(--card);border-radius:var(--radius);padding:16px;text-align:center;border:1px solid var(--border)}
.kpi .value{font-size:1.6rem;font-weight:700;color:var(--navy)}
.kpi .value.good{color:var(--green)}.kpi .value.bad{color:var(--red)}.kpi .value.warn{color:var(--amber)}
.kpi .label{font-size:.75rem;color:var(--muted);margin-top:4px}
.alert{display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:var(--radius);margin-bottom:10px;font-size:.9rem}
.alert.critical{background:#FFE5E5;border-left:4px solid var(--red)}.alert.warning{background:#FFF8E5;border-left:4px solid var(--yellow)}.alert.info{background:#E5F2FF;border-left:4px solid #007AFF}
.alert .icon{font-size:1.3rem}
table{width:100%;border-collapse:collapse;font-size:.88rem}
th{text-align:left;padding:10px 8px;color:var(--muted);font-weight:500;border-bottom:1px solid var(--border)}
td{padding:12px 8px;border-bottom:1px solid var(--border)}tr:last-child td{border-bottom:none}
.badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:.75rem;font-weight:600}.badge.green{background:#E5F9ED;color:#1A7F3C}.badge.red{background:#FFE5E5;color:#C41E3A}.badge.yellow{background:#FFF8E5;color:#B8860B}.badge.blue{background:#E5F2FF;color:#007AFF}
.btn{display:inline-block;padding:10px 20px;border-radius:10px;border:none;background:var(--navy);color:#fff;font-size:.9rem;font-weight:600;cursor:pointer;margin-top:8px}
.btn.secondary{background:var(--bg);color:var(--navy);border:1px solid var(--border)}.btn.red{background:var(--red)}.btn.green{background:var(--green)}.btn.small{padding:6px 12px;font-size:.8rem}
#bottom-nav{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid var(--border);display:flex;justify-content:space-around;padding:8px 0;z-index:100}
#bottom-nav button{background:none;border:none;font-size:.7rem;color:var(--muted);display:flex;flex-direction:column;align-items:center;gap:3px;cursor:pointer;padding:4px 12px}
#bottom-nav button.active{color:var(--navy)}#bottom-nav button .emoji{font-size:1.3rem}
.service-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border)}.service-row:last-child{border-bottom:none}
.service-name{font-weight:600}.service-meta{font-size:.8rem;color:var(--muted)}
.loading{text-align:center;padding:40px;color:var(--muted)}
.error{text-align:center;padding:20px;color:var(--red);font-size:.9rem}
.success{text-align:center;padding:20px;color:var(--green);font-size:.9rem}
input,textarea,select{width:100%;padding:10px;border:1px solid var(--border);border-radius:8px;font-size:.9rem;font-family:inherit;margin-bottom:8px}
.form-group{margin-bottom:12px}
.form-group label{display:block;font-size:.85rem;font-weight:600;margin-bottom:4px;color:var(--muted)}
.modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center;z-index:200;padding:20px}
.modal.active{display:flex}
.modal-content{background:#fff;border-radius:var(--radius);padding:20px;max-width:600px;width:100%;max-height:90vh;overflow-y:auto}
.modal-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
.modal-header h2{font-size:1.2rem}
.close-btn{background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted)}
.json-view{font-family:monospace;font-size:.8rem;background:var(--bg);padding:12px;border-radius:8px;overflow-x:auto;max-height:300px;overflow-y:auto}
#map,#overview-map{height:400px;border-radius:var(--radius);margin-bottom:12px}
#qz-map{height:350px;border-radius:var(--radius);margin-bottom:12px}
.search-box{display:flex;gap:8px;margin-bottom:12px}
.search-box input{flex:1;margin-bottom:0}
.tabs{display:flex;gap:8px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:8px;overflow-x:auto}
.tabs button{background:none;border:none;padding:8px 16px;font-size:.9rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap}
.tabs button.active{color:var(--navy);border-bottom-color:var(--navy);font-weight:600}
.filter-row{display:flex;gap:8px;margin-bottom:12px;overflow-x:auto}
.filter-row button{padding:6px 12px;border-radius:16px;border:1px solid var(--border);background:#fff;font-size:.8rem;cursor:pointer;white-space:nowrap}
.filter-row button.active{background:var(--navy);color:#fff;border-color:var(--navy)}
.support-ticket{border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:8px;cursor:pointer}
.support-ticket:hover{background:var(--bg)}
.support-ticket .ticket-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.support-ticket .ticket-title{font-weight:600}
.support-ticket .ticket-meta{font-size:.8rem;color:var(--muted);margin-top:4px}
.mission-detail{padding:16px;background:var(--bg);border-radius:8px;margin-top:12px}
.mission-detail h4{margin-bottom:8px;color:var(--navy)}
.mission-detail p{margin-bottom:4px;font-size:.9rem}
.timeline{position:relative;padding-left:24px;margin-top:12px}
.timeline::before{content:'';position:absolute;left:8px;top:0;bottom:0;width:2px;background:var(--border)}
.timeline-item{position:relative;margin-bottom:16px}
.timeline-item::before{content:'';position:absolute;left:-20px;top:4px;width:10px;height:10px;border-radius:50%;background:var(--amber);border:2px solid #fff}
.timeline-item.completed::before{background:var(--green)}
.timeline-item.pending::before{background:var(--muted)}
.timeline-date{font-size:.75rem;color:var(--muted)}
.timeline-text{font-size:.9rem;margin-top:2px}
.chart-bar{display:flex;align-items:center;margin-bottom:6px}
.chart-bar-label{width:100px;font-size:.8rem;color:var(--muted)}
.chart-bar-fill{height:20px;background:var(--navy);border-radius:4px;transition:width .3s}
.chart-bar-value{margin-left:8px;font-size:.8rem;font-weight:600}
.action-menu{position:absolute;right:0;top:40px;background:#fff;border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.1);display:none;z-index:50}
.action-menu.active{display:block}
.action-menu button{display:block;width:100%;padding:10px 16px;border:none;background:none;text-align:left;cursor:pointer;font-size:.9rem}
.action-menu button:hover{background:var(--bg)}
</style>
</head>
<body>
<div id="header"><div class="logo">🔁</div><h1>AAMOS Command Center</h1><div class="status" id="clock">--:--</div></div>
<div id="nav">
<button class="active" onclick="showSection('overview')">Översikt</button>
<button onclick="showSection('finance')">Ekonomi</button>
<button onclick="showSection('quixzoom')">quiXzoom</button>
<button onclick="showSection('support')">Support</button>
<button onclick="showSection('apis')">API:er</button>
<button onclick="showSection('systems')">System</button>
</div>
<!-- ÖVERSIKT -->
<div id="overview" class="section active">
<div class="kpi-row">
<div class="kpi"><div class="value" id="kpi-cash">--</div><div class="label">Kassa (SEK)</div></div>
<div class="kpi"><div class="value" id="kpi-missions">--</div><div class="label">Missions</div></div>
<div class="kpi"><div class="value" id="kpi-entries">--</div><div class="label">Verifikat</div></div>
<div class="kpi"><div class="value" id="kpi-services">--</div><div class="label">Tjänster</div></div>
<div class="kpi"><div class="value" id="kpi-tickets">--</div><div class="label">Ärenden</div></div>
</div>
<div class="card"><h3>🚨 Kritiska åtgärder <span class="live">● LIVE</span></h3><div id="critical-alerts"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3>🗺️ Operationskarta <span class="live">● LIVE</span></h3><div id="overview-map"></div></div>
<div class="card"><h3>📈 Aktivitet senaste 7 dagarna</h3><div id="activity-chart"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3>🖥️ Systemstatus <span class="live">● LIVE</span></h3><div id="system-status"><div class="loading">Laddar...</div></div></div>
</div>
<!-- EKONOMI -->
<div id="finance" class="section">
<div class="kpi-row">
<div class="kpi"><div class="value" id="fin-accounts">--</div><div class="label">Konton</div></div>
<div class="kpi"><div class="value" id="fin-periods">--</div><div class="label">Perioder</div></div>
<div class="kpi"><div class="value" id="fin-entries">--</div><div class="label">Verifikat</div></div>
<div class="kpi"><div class="value" id="fin-balance">--</div><div class="label">Total balans</div></div>
</div>
<div class="card"><h3>🔍 Sök verifikat</h3><div class="search-box"><input type="text" id="journal-search" placeholder="Sök beskrivning, konto, belopp, datum..." onkeyup="searchJournal()"><button class="btn" onclick="searchJournal()">🔍 Sök</button></div></div>
<div class="card"><h3>🏦 Konton <span class="live">● LIVE</span></h3><div class="filter-row"><button class="active" onclick="filterAccounts('all')">Alla</button><button onclick="filterAccounts('asset')">Tillgångar</button><button onclick="filterAccounts('liability')">Skulder</button><button onclick="filterAccounts('income')">Intäkter</button><button onclick="filterAccounts('expense')">Kostnader</button></div><div id="accounts-list"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3>📅 Perioder <span class="live">● LIVE</span></h3><table id="periods-table"><tr><th>Period</th><th>År</th><th>Status</th><th>Stängd</th><th>Åtgärd</th></tr><tr><td colspan="5" class="loading">Laddar...</td></tr></table></div>
<div class="card"><h3>📊 Verifikat <span class="live">● LIVE</span></h3><div id="journal-list"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3>📈 Balansfördelning</h3><div id="balance-chart"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3> Nytt verifikat</h3><button class="btn" onclick="openModal('journal-modal')">📄 Skapa verifikat</button></div>
<div class="card"><h3>🔗 Export & Rapporter</h3><a class="btn" href="/api/ledger/export/sie4?fiscal_year=2026" target="_blank">📥 SIE4 Export 2026</a><button class="btn secondary" onclick="generateReport()">📊 Generera rapport</button></div>
</div>
<!-- QUIXZOOM -->
<div id="quixzoom" class="section">
<div class="kpi-row">
<div class="kpi"><div class="value" id="qz-missions">--</div><div class="label">Missions</div></div>
<div class="kpi"><div class="value" id="qz-open">--</div><div class="label">Öppna</div></div>
<div class="kpi"><div class="value" id="qz-assigned">--</div><div class="label">Tilldelade</div></div>
<div class="kpi"><div class="value" id="qz-reward">--</div><div class="label">Reward-pool</div></div>
</div>
<div class="card"><h3>🔍 Sök missions</h3><div class="search-box"><input type="text" id="mission-search" placeholder="Sök titel, adress, status, zoomer..." onkeyup="searchMissions()"><button class="btn" onclick="searchMissions()">🔍 Sök</button></div></div>
<div class="card"><h3>🗺️ Missionskarta</h3><div id="qz-map"></div></div>
<div class="card"><h3>📱 Missions <span class="live">● LIVE</span></h3><div class="filter-row">
<button class="active" onclick="filterMissions('all')">Alla</button>
<button onclick="filterMissions('open')">Öppna</button>
<button onclick="filterMissions('assigned')">Tilldelade</button>
<button onclick="filterMissions('submitted')">Inskickade</button>
<button onclick="filterMissions('approved')">Godkända</button>
</div><div id="missions-list"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3>📊 Mission-statistik</h3><div id="mission-stats"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3> Nytt uppdrag</h3><button class="btn" onclick="openModal('mission-modal')">🎯 Skapa mission</button></div>
</div>
<!-- SUPPORT -->
<div id="support" class="section">
<div class="kpi-row">
<div class="kpi"><div class="value" id="sup-open">--</div><div class="label">Öppna</div></div>
<div class="kpi"><div class="value" id="sup-critical">--</div><div class="label">Kritiska</div></div>
<div class="kpi"><div class="value" id="sup-today">--</div><div class="label">Idag</div></div>
<div class="kpi"><div class="value" id="sup-avg">--</div><div class="label">Snittid (h)</div></div>
</div>
<div class="card"><h3>🔍 Sök ärenden</h3><div class="search-box"><input type="text" id="ticket-search" placeholder="Sök ärende, zoomer, mission, beskrivning..." onkeyup="searchTickets()"><button class="btn" onclick="searchTickets()">🔍 Sök</button></div></div>
<div class="card"><h3>📋 Supportärenden</h3><div class="tabs">
<button class="active" onclick="filterTickets('all')">Alla</button>
<button onclick="filterTickets('open')">Öppna</button>
<button onclick="filterTickets('critical')">Kritiska</button>
<button onclick="filterTickets('resolved')">Lösta</button>
</div><div id="tickets-list"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3> Nytt ärende</h3><button class="btn" onclick="openModal('ticket-modal')">🎫 Skapa ärende</button></div>
</div>
<!-- API:ER -->
<div id="apis" class="section">
<div class="card"><h3>🔌 API-administration <span class="live">● LIVE</span></h3><div id="api-list"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3>🌍 Landvex Intelligence</h3>
<div class="form-group"><label>Latitud</label><input type="number" id="intel-lat" placeholder="59.3349" step="0.0001"></div>
<div class="form-group"><label>Longitud</label><input type="number" id="intel-lng" placeholder="18.0632" step="0.0001"></div>
<div class="form-group"><label>Profil</label><select id="intel-profile"><option value="business">Business</option><option value="infrastructure">Infrastructure</option><option value="safety">Safety</option><option value="investment">Investment</option><option value="nightlife">Nightlife</option><option value="tourist">Tourist</option></select></div>
<button class="btn" onclick="testIntelligence()">🔍 Analysera plats</button>
<div id="intel-result" style="margin-top:12px"></div>
</div>
<div class="card"><h3>📡 Batch-analys</h3>
<div class="form-group"><label>Punkter (JSON-format: [{"lat":x,"lng":y}])</label><textarea id="batch-points" rows="3" placeholder='[{"lat":59.3349,"lng":18.0632},{"lat":59.3421,"lng":18.0559}]'></textarea></div>
<div class="form-group"><label>Profil</label><select id="batch-profile"><option value="business">Business</option><option value="infrastructure">Infrastructure</option></select></div>
<button class="btn" onclick="testBatchIntelligence()">🚀 Kör batch</button>
<div id="batch-result" style="margin-top:12px"></div>
</div>
</div>
<!-- SYSTEM -->
<div id="systems" class="section">
<div class="card"><h3>🖥️ Tjänster <span class="live">● LIVE</span></h3><div id="services-list"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3>📋 Systemlogg</h3><div class="tabs">
<button class="active" onclick="showLog('ledger')">Ledger</button>
<button onclick="showLog('mission')">Mission</button>
<button onclick="showLog('avo')">AVO</button>
<button onclick="showLog('gateway')">Gateway</button>
</div><div id="system-log" style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:12px;border-radius:8px;max-height:400px;overflow-y:auto"><div class="loading">Laddar...</div></div></div>
<div class="card"><h3>⚡ Åtgärder</h3><button class="btn" onclick="restartService('aamos-ledger')">🔄 Starta om Ledger</button><button class="btn secondary" onclick="restartService('quixzoom-mission')">🔄 Starta om Mission</button><button class="btn secondary" onclick="restartService('quixzoom-avo')">🔄 Starta om AVO</button><div id="restart-result" style="margin-top:12px"></div></div>
</div>
<!-- MODALS -->
<div id="journal-modal" class="modal">
<div class="modal-content">
<div class="modal-header"><h2>📄 Nytt verifikat</h2><button class="close-btn" onclick="closeModal('journal-modal')">&times;</button></div>
<div class="form-group"><label>Datum</label><input type="date" id="jv-date"></div>
<div class="form-group"><label>Beskrivning</label><input type="text" id="jv-desc" placeholder="T.ex. Faktura #1234"></div>
<div class="form-group"><label>Referens</label><input type="text" id="jv-ref" placeholder="T.ex. FAK-2026-001"></div>
<div class="form-group"><label>Konto (debet)</label><input type="text" id="jv-debit-acc" placeholder="T.ex. 1930"></div>
<div class="form-group"><label>Belopp (debet)</label><input type="number" id="jv-debit-amount" placeholder="0"></div>
<div class="form-group"><label>Konto (kredit)</label><input type="text" id="jv-credit-acc" placeholder="T.ex. 3000"></div>
<div class="form-group"><label>Belopp (kredit)</label><input type="number" id="jv-credit-amount" placeholder="0"></div>
<button class="btn" onclick="createJournalEntry()">💾 Spara verifikat</button>
<div id="jv-result" style="margin-top:12px"></div>
</div>
</div>
<div id="mission-modal" class="modal">
<div class="modal-content">
<div class="modal-header"><h2>🎯 Nytt uppdrag</h2><button class="close-btn" onclick="closeModal('mission-modal')">&times;</button></div>
<div class="form-group"><label>Titel</label><input type="text" id="m-title" placeholder="T.ex. Fasadinspektion"></div>
<div class="form-group"><label>Beskrivning</label><textarea id="m-desc" rows="3" placeholder="Beskriv vad som ska göras..."></textarea></div>
<div class="form-group"><label>Adress</label><input type="text" id="m-address" placeholder="T.ex. Kungsgatan 12, Stockholm"></div>
<div class="form-group"><label>Latitud</label><input type="number" id="m-lat" placeholder="59.3349" step="0.0001"></div>
<div class="form-group"><label>Longitud</label><input type="number" id="m-lng" placeholder="18.0632" step="0.0001"></div>
<div class="form-group"><label>Reward (SEK)</label><input type="number" id="m-reward" placeholder="1500"></div>
<div class="form-group"><label>Typ</label><select id="m-type"><option value="facade_scan">Fasadscan</option><option value="road_inspection">Väginspektion</option><option value="damage_doc">Skadedokumentation</option><option value="general">Generell</option></select></div>
<button class="btn" onclick="createMission()">💾 Spara uppdrag</button>
<div id="m-result" style="margin-top:12px"></div>
</div>
</div>
<div id="ticket-modal" class="modal">
<div class="modal-content">
<div class="modal-header"><h2>🎫 Nytt supportärende</h2><button class="close-btn" onclick="closeModal('ticket-modal')">&times;</button></div>
<div class="form-group"><label>Ärendetyp</label><select id="t-type"><option value="payment">Betalningsfråga</option><option value="mission">Mission-problem</option><option value="account">Kontoåtkomst</option><option value="technical">Tekniskt fel</option><option value="other">Annat</option></select></div>
<div class="form-group"><label>Zoomer ID (valfritt)</label><input type="text" id="t-zoomer" placeholder="T.ex. zoomer-abc-123"></div>
<div class="form-group"><label>Mission ID (valfritt)</label><input type="text" id="t-mission" placeholder="T.ex. mission-xxx"></div>
<div class="form-group"><label>Beskrivning</label><textarea id="t-desc" rows="4" placeholder="Beskriv problemet..."></textarea></div>
<div class="form-group"><label>Prioritet</label><select id="t-priority"><option value="low">Låg</option><option value="medium" selected>Medium</option><option value="high">Hög</option><option value="critical">Kritisk</option></select></div>
<button class="btn" onclick="createTicket()">💾 Spara ärende</button>
<div id="t-result" style="margin-top:12px"></div>
</div>
</div>
<div id="mission-detail-modal" class="modal">
<div class="modal-content">
<div class="modal-header"><h2>🎯 Mission-detaljer</h2><button class="close-btn" onclick="closeModal('mission-detail-modal')">&times;</button></div>
<div id="mission-detail-content"><div class="loading">Laddar...</div></div>
</div>
</div>
<div id="bottom-nav">
<button class="active" onclick="showSection('overview')"><span class="emoji">📊</span>Översikt</button>
<button onclick="showSection('finance')"><span class="emoji">🏦</span>Ekonomi</button>
<button onclick="showSection('quixzoom')"><span class="emoji">📸</span>quiXzoom</button>
<button onclick="showSection('support')"><span class="emoji">🎧</span>Support</button>
</div>
<script>
const API_BASE='/api/ledger',QZ_API='/api/quixzoom',AVO_API='/api/avo',GATEWAY='/api/gateway';
let overviewMap,qzMap,allMissions=[],allJournal=[],allAccounts=[],allTickets=[];
function updateClock(){const n=new Date();document.getElementById('clock').textContent=n.toLocaleTimeString('sv-SE',{hour:'2-digit',minute:'2-digit'})}
setInterval(updateClock,1000);updateClock();
function showSection(id){document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));document.querySelectorAll('#nav button,#bottom-nav button').forEach(b=>b.classList.remove('active'));document.getElementById(id).classList.add('active');if(event&&event.target)event.target.classList.add('active');if(id==='overview'&&!overviewMap)setTimeout(initOverviewMap,100);if(id==='quixzoom'&&!qzMap)setTimeout(initQzMap,100)}
function openModal(id){document.getElementById(id).classList.add('active')}
function closeModal(id){document.getElementById(id).classList.remove('active')}
function formatSEK(n){if(n===null||n===undefined)return'--';return new Intl.NumberFormat('sv-SE').format(Math.round(n))+' kr'}
function formatDate(d){if(!d)return'-';return new Date(d).toLocaleDateString('sv-SE')}
function statusBadge(s){const m={running:'green',active:'green',ok:'green',operational:'green',stopped:'red',failed:'red',error:'red',critical:'red',warning:'yellow',warn:'yellow',degraded:'yellow',open:'yellow',closed:'green',pending:'blue',assigned:'blue',submitted:'amber',approved:'green',resolved:'green'};return'<span class="badge '+(m[s]||'blue')+'">'+s+'</span>'}
// === KARTOR ===
function initOverviewMap(){overviewMap=L.map('overview-map').setView([59.3349,18.0632],11);L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OpenStreetMap'}).addTo(overviewMap);loadMissionMarkers(overviewMap)}
function initQzMap(){qzMap=L.map('qz-map').setView([59.3349,18.0632],11);L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OpenStreetMap'}).addTo(qzMap);loadMissionMarkers(qzMap)}
function loadMissionMarkers(mapObj){if(!mapObj)return;fetch(QZ_API+'/missions').then(r=>r.json()).then(d=>{const missions=d.missions||[];missions.forEach(m=>{if(m.lat&&m.lon){const color=m.status==='open'?'#34C759':m.status==='assigned'?'#007AFF':m.status==='submitted'?'#FFCC00':'#6B6B70';L.circleMarker([m.lat,m.lon],{radius:8,fillColor:color,color:'#fff',weight:2,opacity:1,fillOpacity:.8}).addTo(mapObj).bindPopup('<b>'+m.title+'</b><br>'+m.address+'<br>'+formatSEK(m.reward_sek)+'<br>Status: '+m.status+'<br><button onclick="showMissionDetail(\''+m.id+'\')" style="margin-top:4px;padding:4px 8px;background:#001E50;color:#fff;border:none;border-radius:4px;cursor:pointer">Visa detaljer</button>')}})})}
// === EKONOMI ===
async function loadAccounts(){try{const r=await fetch(API_BASE+'/accounts'),d=await r.json();if(!d.ok||!d.accounts)throw new Error('Inga konton');allAccounts=d.accounts.filter(x=>x.tenant_id==='landvex');document.getElementById('fin-accounts').textContent=allAccounts.length;const bank=allAccounts.find(x=>x.account_number==='1930');if(bank){const bal=(bank.debit||0)-(bank.credit||0);document.getElementById('kpi-cash').textContent=formatSEK(bal);document.getElementById('kpi-cash').className=bal<0?'value bad':'value good'}renderAccounts(allAccounts);renderBalanceChart(allAccounts)}catch(e){document.getElementById('accounts-list').innerHTML='<div class="error">Kunde inte ladda konton</div>'}}
function renderAccounts(accounts){const rows=accounts.map(x=>{const bal=(x.debit||0)-(x.credit||0);return'<div class="service-row"><div><div class="service-name">'+x.account_number+' '+x.name+'</div><div class="service-meta">'+x.account_type+'</div></div><div class="value '+(bal<0?'bad':bal>0?'good':'')+'">'+formatSEK(bal)+'</div></div>'}).join('');document.getElementById('accounts-list').innerHTML=rows||'<div class="error">Inga konton hittades</div>'}
function filterAccounts(type){document.querySelectorAll('#finance .filter-row button').forEach(b=>b.classList.remove('active'));event.target.classList.add('active');if(type==='all'){renderAccounts(allAccounts)}else{renderAccounts(allAccounts.filter(a=>a.account_type===type))}}
function renderBalanceChart(accounts){const types={asset:0,liability:0,income:0,expense:0};accounts.forEach(a=>{const bal=(a.debit||0)-(a.credit||0);if(types[a.account_type]!==undefined)types[a.account_type]+=Math.abs(bal)});const max=Math.max(...Object.values(types))||1;const labels={asset:'Tillgångar',liability:'Skulder',income:'Intäkter',expense:'Kostnader'};const colors={asset:'#34C759',liability:'#FF3B30',income:'#007AFF',expense:'#FFCC00'};let html='';Object.entries(types).forEach(([type,value])=>{const pct=(value/max*100).toFixed(1);html+='<div class="chart-bar"><div class="chart-bar-label">'+labels[type]+'</div><div class="chart-bar-fill" style="width:'+pct+'%;background:'+colors[type]+'"></div><div class="chart-bar-value">'+formatSEK(value)+'</div></div>'});document.getElementById('balance-chart').innerHTML=html;const total=accounts.reduce((s,a)=>s+((a.debit||0)-(a.credit||0)),0);document.getElementById('fin-balance').textContent=formatSEK(total)}
async function loadPeriods(){try{const r=await fetch(API_BASE+'/periods'),d=await r.json();if(!d.ok||!d.periods)throw new Error('Inga perioder');document.getElementById('fin-periods').textContent=d.periods.length;const rows=d.periods.slice(0,12).map(p=>'<tr><td>'+p.period+'</td><td>'+p.fiscal_year+'</td><td>'+statusBadge(p.status)+'</td><td>'+(p.closed_at?formatDate(p.closed_at):'-')+'</td><td>'+(p.status==='open'?'<button class="btn small" onclick="closePeriod(\''+p.period+'\')">Stäng</button>':'-')+'</td></tr>').join('');document.getElementById('periods-table').innerHTML='<tr><th>Period</th><th>År</th><th>Status</th><th>Stängd</th><th>Åtgärd</th></tr>'+rows}catch(e){document.getElementById('periods-table').innerHTML='<tr><td colspan="5" class="error">Kunde inte ladda perioder</td></tr>'}}
async function closePeriod(period){if(!confirm('Stäng period '+period+'?'))return;try{const r=await fetch(API_BASE+'/periods/'+period+'/close',{method:'POST'});const d=await r.json();if(d.ok){alert('Period '+period+' stängd!');loadPeriods()}else{alert('Fel: '+(d.error||'Kunde inte stänga period'))}}catch(e){alert('Fel: Kunde inte stänga period')}}
async function loadJournal(){try{const r=await fetch(API_BASE+'/journal'),d=await r.json();if(!d.ok||!d.entries)throw new Error('Inga verifikat');allJournal=d.entries||[];document.getElementById('fin-entries').textContent=allJournal.length;document.getElementById('kpi-entries').textContent=allJournal.length;renderJournal(allJournal.slice(-20).reverse())}catch(e){document.getElementById('journal-list').innerHTML='<div class="error">Kunde inte ladda verifikat</div>'}}
function renderJournal(entries){const rows=entries.map(e=>{const amt=e.lines?e.lines.reduce((s,l)=>s+(l.debit||0),0):0;return'<div class="service-row"><div><div class="service-name">#'+e.entry_number+' '+e.description+'</div><div class="service-meta">'+e.entry_date+' • '+e.period+(e.reference?' • Ref: '+e.reference:'')+'</div></div><div class="value">'+formatSEK(amt)+'</div></div>'}).join('');document.getElementById('journal-list').innerHTML=rows||'<div class="error">Inga verifikat</div>'}
function searchJournal(){const q=document.getElementById('journal-search').value.toLowerCase();if(!q){renderJournal(allJournal.slice(-20).reverse());return}const filtered=allJournal.filter(e=>e.description.toLowerCase().includes(q)||e.entry_number.toString().includes(q)||(e.lines&&e.lines.some(l=>(l.account_number||'').includes(q)))||e.entry_date.includes(q));renderJournal(filtered.slice(-50).reverse())}
async function createJournalEntry(){const date=document.getElementById('jv-date').value,desc=document.getElementById('jv-desc').value,ref=document.getElementById('jv-ref').value,dacc=document.getElementById('jv-debit-acc').value,damt=parseFloat(document.getElementById('jv-debit-amount').value),cacc=document.getElementById('jv-credit-acc').value,camt=parseFloat(document.getElementById('jv-credit-amount').value);if(!date||!desc||!dacc||!cacc||!damt||!camt){document.getElementById('jv-result').innerHTML='<div class="alert critical"><span class="icon">⚠️</span><div>Fyll i alla fält</div></div>';return}try{const r=await fetch(API_BASE+'/journal',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({entry_date:date,description:desc,reference:ref||null,fiscal_year:2026,tenant_id:'landvex',source_type:'manual',lines:[{account_number:dacc,debit:damt||0,credit:0},{account_number:cacc,debit:0,credit:camt||0}]})});const data=await r.json();if(data.ok){document.getElementById('jv-result').innerHTML='<div class="alert info"><span class="icon">✅</span><div>Verifikat skapat! #'+data.entry_number+'</div></div>';loadJournal();setTimeout(()=>closeModal('journal-modal'),1500)}else{document.getElementById('jv-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div>'+(data.error||'Fel')+'</div></div>'}}catch(e){document.getElementById('jv-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div>Kunde inte skapa verifikat</div></div>'}}
function generateReport(){const win=window.open('','_blank');win.document.write('<html><head><title>Ekonomisk rapport</title><style>body{font-family:Arial;padding:40px}h1{color:#001E50}table{width:100%;border-collapse:collapse}th,td{padding:10px;text-align:left;border-bottom:1px solid #ddd}th{background:#001E50;color:#fff}</style></head><body><h1>Landvex AB — Ekonomisk rapport</h1><p>Genererad: '+new Date().toLocaleString('sv-SE')+'</p><h2>Konton</h2><table><tr><th>Konto</th><th>Namn</th><th>Typ</th><th>Saldo</th></tr>'+allAccounts.map(a=>'<tr><td>'+a.account_number+'</td><td>'+a.name+'</td><td>'+a.account_type+'</td><td>'+formatSEK((a.debit||0)-(a.credit||0))+'</td></tr>').join('')+'</table><h2>Senaste verifikat</h2><table><tr><th>#</th><th>Datum</th><th>Beskrivning</th><th>Belopp</th></tr>'+allJournal.slice(-10).reverse().map(e=>'<tr><td>'+e.entry_number+'</td><td>'+e.entry_date+'</td><td>'+e.description+'</td><td>'+formatSEK(e.lines?e.lines.reduce((s,l)=>s+(l.debit||0),0):0)+'</td></tr>').join('')+'</table></body></html>')}
// === QUIXZOOM ===
async function loadMissions(){try{const r=await fetch(QZ_API+'/missions'),d=await r.json();allMissions=d.missions||[];document.getElementById('qz-missions').textContent=allMissions.length;document.getElementById('kpi-missions').textContent=allMissions.length;const open=allMissions.filter(x=>x.status==='open').length;document.getElementById('qz-open').textContent=open;const assigned=allMissions.filter(x=>x.status==='assigned').length;document.getElementById('qz-assigned').textContent=assigned;const totalReward=allMissions.reduce((s,x)=>s+(x.reward_sek||0),0);document.getElementById('qz-reward').textContent=formatSEK(totalReward);renderMissions(allMissions);renderMissionStats(allMissions)}catch(e){document.getElementById('missions-list').innerHTML='<div class="error">Kunde inte ladda missions</div>'}}
function renderMissions(missions){const rows=missions.map(x=>'<div class="service-row" style="cursor:pointer" onclick="showMissionDetail(\''+x.id+'\')"><div><div class="service-name">'+x.title+'</div><div class="service-meta">'+x.address+' • '+formatSEK(x.reward_sek)+(x.zoomer_id?' • Zoomer: '+x.zoomer_id:'')+'</div></div><div>'+statusBadge(x.status)+'</div></div>').join('');document.getElementById('missions-list').innerHTML=rows||'<div class="error">Inga missions</div>'}
function filterMissions(status){document.querySelectorAll('#quixzoom .filter-row button').forEach(b=>b.classList.remove('active'));event.target.classList.add('active');if(status==='all'){renderMissions(allMissions)}else{renderMissions(allMissions.filter(m=>m.status===status))}}
function searchMissions(){const q=document.getElementById('mission-search').value.toLowerCase();if(!q){renderMissions(allMissions);return}const filtered=allMissions.filter(m=>m.title.toLowerCase().includes(q)||m.address.toLowerCase().includes(q)||m.status.includes(q)||m.id.includes(q)||(m.zoomer_id&&m.zoomer_id.includes(q)));renderMissions(filtered)}
function renderMissionStats(missions){const stats={open:0,assigned:0,submitted:0,approved:0};missions.forEach(m=>{if(stats[m.status]!==undefined)stats[m.status]++});const total=missions.length||1;const labels={open:'Öppna',assigned:'Tilldelade',submitted:'Inskickade',approved:'Godkända'};let html='';Object.entries(stats).forEach(([status,count])=>{const pct=(count/total*100).toFixed(1);html+='<div class="chart-bar"><div class="chart-bar-label">'+labels[status]+'</div><div class="chart-bar-fill" style="width:'+(pct>0?pct:5)+'%"></div><div class="chart-bar-value">'+count+' ('+pct+'%)</div></div>'});document.getElementById('mission-stats').innerHTML=html}
async function showMissionDetail(missionId){const mission=allMissions.find(m=>m.id===missionId);if(!mission)return;const timeline=[{date:mission.created_at,text:'Mission skapad',completed:true},{date:mission.accepted_at,text:'Tilldelad till zoomer'+(mission.zoomer_id?': '+mission.zoomer_id:''),completed:!!mission.accepted_at},{date:mission.submitted_at,text:'Inskickad av zoomer',completed:!!mission.submitted_at},{date:mission.approved_at,text:'Godkänd & betalad',completed:!!mission.approved_at}];let html='<div class="mission-detail"><h4>'+mission.title+'</h4><p><strong>ID:</strong> '+mission.id+'</p><p><strong>Adress:</strong> '+mission.address+'</p><p><strong>Koordinater:</strong> '+mission.lat+', '+mission.lon+'</p><p><strong>Reward:</strong> '+formatSEK(mission.reward_sek)+'</p><p><strong>Typ:</strong> '+mission.mission_type+'</p><p><strong>Status:</strong> '+statusBadge(mission.status)+'</p>'+(mission.brief_url?'<p><strong>Brief:</strong> <a href="'+mission.brief_url+'" target="_blank">Öppna brief</a></p>':'')+'<h4 style="margin-top:16px">Tidslinje</h4><div class="timeline">'+timeline.map(t=>'<div class="timeline-item '+(t.completed?'completed':'pending')+'"><div class="timeline-date">'+(t.date?formatDate(t.date):'Väntar...')+'</div><div class="timeline-text">'+t.text+'</div></div>').join('')+'</div></div>';document.getElementById('mission-detail-content').innerHTML=html;openModal('mission-detail-modal')}
async function createMission(){const title=document.getElementById('m-title').value,desc=document.getElementById('m-desc').value,address=document.getElementById('m-address').value,lat=parseFloat(document.getElementById('m-lat').value),lng=parseFloat(document.getElementById('m-lng').value),reward=parseInt(document.getElementById('m-reward').value),type=document.getElementById('m-type').value;if(!title||!desc||!address||!lat||!lng||!reward){document.getElementById('m-result').innerHTML='<div class="alert critical"><span class="icon">⚠️</span><div>Fyll i alla fält</div></div>';return}try{const r=await fetch(QZ_API+'/missions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:title,description:desc,address:address,lat:lat,lon:lng,reward_sek:reward,mission_type:type,status:'open'})});const data=await r.json();if(data.id||data.mission){document.getElementById('m-result').innerHTML='<div class="alert info"><span class="icon">✅</span><div>Mission skapat! ID: '+(data.id||data.mission.id)+'</div></div>';loadMissions();setTimeout(()=>closeModal('mission-modal'),1500)}else{document.getElementById('m-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div>'+(data.error||'Fel')+'</div></div>'}}catch(e){document.getElementById('m-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div>Kunde inte skapa mission</div></div>'}}
// === SUPPORT ===
function loadTickets(){allTickets=[{id:'SUP-001',type:'payment',zoomer:'zoomer-abc-123',mission:'mission-ed96af2d',desc:'Betalning inte mottagen för mission-ed96af2d',priority:'high',status:'open',created:'2026-07-02T10:00:00Z',updated:'2026-07-02T14:00:00Z',comments:[]},{id:'SUP-002',type:'mission',zoomer:null,mission:'mission-7ca2831f',desc:'Felaktig adress i uppdrag Strandvägen',priority:'medium',status:'open',created:'2026-07-02T12:00:00Z',updated:'2026-07-02T12:00:00Z',comments:[]},{id:'SUP-003',type:'account',zoomer:'zoomer-def-456',mission:null,desc:'Kan inte logga in på appen',priority:'low',status:'resolved',created:'2026-07-01T09:00:00Z',updated:'2026-07-02T10:00:00Z',comments:[{user:'Support',text:'Återställt lösenord',date:'2026-07-02T10:00:00Z'}]},{id:'SUP-004',type:'technical',zoomer:'zoomer-ghi-789',mission:'mission-0ce4813a',desc:'Appen kraschar vid uppladdning av bilder',priority:'critical',status:'open',created:'2026-07-03T08:00:00Z',updated:'2026-07-03T08:00:00Z',comments:[]}];renderTickets(allTickets)}
function renderTickets(tickets){const open=tickets.filter(t=>t.status==='open').length;const critical=tickets.filter(t=>t.priority==='critical'&&t.status==='open').length;document.getElementById('sup-open').textContent=open;document.getElementById('sup-critical').textContent=critical;document.getElementById('kpi-tickets').textContent=open;document.getElementById('sup-today').textContent=tickets.filter(t=>t.created.startsWith(new Date().toISOString().slice(0,10))).length;document.getElementById('sup-avg').textContent='2.5';const rows=tickets.map(t=>'<div class="support-ticket" onclick="showTicketDetail(\''+t.id+'\')"><div class="ticket-header"><div><span class="ticket-title">'+t.id+'</span> '+statusBadge(t.status)+'</div><span class="badge '+(t.priority==='critical'?'red':t.priority==='high'?'yellow':'blue')+'">'+t.priority+'</span></div><div class="ticket-meta">'+(t.zoomer?'Zoomer: '+t.zoomer+' • ':'')+(t.mission?'Mission: '+t.mission+' • ':'')+formatDate(t.created)+' • '+t.desc.slice(0,60)+(t.desc.length>60?'...':'')+'</div></div>').join('');document.getElementById('tickets-list').innerHTML=rows||'<div class="error">Inga ärenden</div>'}
function filterTickets(filter){document.querySelectorAll('#support .tabs button').forEach(b=>b.classList.remove('active'));event.target.classList.add('active');if(filter==='all'){renderTickets(allTickets)}else if(filter==='open'){renderTickets(allTickets.filter(t=>t.status==='open'))}else if(filter==='critical'){renderTickets(allTickets.filter(t=>t.priority==='critical'&&t.status==='open'))}else if(filter==='resolved'){renderTickets(allTickets.filter(t=>t.status==='resolved'))}}
function searchTickets(){const q=document.getElementById('ticket-search').value.toLowerCase();if(!q){renderTickets(allTickets);return}const filtered=allTickets.filter(t=>t.id.toLowerCase().includes(q)||t.desc.toLowerCase().includes(q)||(t.zoomer&&t.zoomer.includes(q))||(t.mission&&t.mission.includes(q)));renderTickets(filtered)}
function showTicketDetail(ticketId){const ticket=allTickets.find(t=>t.id===ticketId);if(!ticket)return;const commentsHtml=ticket.comments.length?ticket.comments.map(c=>'<div style="background:var(--bg);padding:8px;border-radius:8px;margin-bottom:8px"><div style="font-size:.8rem;color:var(--muted)">'+c.user+' • '+formatDate(c.date)+'</div><div>'+c.text+'</div></div>').join(''):'<div style="color:var(--muted);font-size:.9rem">Inga kommentarer</div>';alert('Ärende: '+ticket.id+'\nTyp: '+ticket.type+'\nPrioritet: '+ticket.priority+'\nStatus: '+ticket.status+'\nBeskrivning: '+ticket.desc+'\n\nKommentarer:\n'+ticket.comments.map(c=>c.user+': '+c.text).join('\n'))}
function createTicket(){const type=document.getElementById('t-type').value,zoomer=document.getElementById('t-zoomer').value,mission=document.getElementById('t-mission').value,desc=document.getElementById('t-desc').value,priority=document.getElementById('t-priority').value;if(!desc){document.getElementById('t-result').innerHTML='<div class="alert critical">Beskrivning krävs</div>';return}const newTicket={id:'SUP-'+String(allTickets.length+1).padStart(3,'0'),type:type,zoomer:zoomer||null,mission:mission||null,desc:desc,priority:priority,status:'open',created:new Date().toISOString(),updated:new Date().toISOString(),comments:[]};allTickets.unshift(newTicket);renderTickets(allTickets);document.getElementById('t-result').innerHTML='<div class="alert info"><span class="icon">✅</span><div>Ärende skapat: '+newTicket.id+'</div></div>';setTimeout(()=>{closeModal('ticket-modal');document.getElementById('t-result').innerHTML=''},1500)}
// === SYSTEM ===
async function loadSystemStatus(){const svcs=[{name:'aamos-ledger',port:3250,path:'/health'},{name:'quixzoom-mission',port:7060,path:'/health'},{name:'quixzoom-avo',port:7070,path:'/health'},{name:'aamos-gateway',port:3401,path:'/'},{name:'aamos-api-gateway',port:8080,path:'/health'}];const results=await Promise.all(svcs.map(async s=>{try{const r=await fetch('http://localhost:'+s.port+s.path);return{name:s.name,port:s.port,status:r.ok?'running':'error',responseTime:'<50ms'}}catch(e){return{name:s.name,port:s.port,status:'stopped',responseTime:'-'}}));const running=results.filter(r=>r.status==='running').length;document.getElementById('kpi-services').textContent=running+'/'+results.length;const html=results.map(r=>'<div class="service-row"><div><div class="service-name">'+r.name+'</div><div class="service-meta">Port '+r.port+'</div></div><div><span style="font-size:.8rem;color:var(--muted);margin-right:8px">'+r.responseTime+'</span>'+statusBadge(r.status)+'</div></div>').join('');document.getElementById('system-status').innerHTML=html;document.getElementById('services-list').innerHTML=html}
async function loadSystemLog(){try{const r=await fetch('/api/system/logs?lines=50&service=ledger');const d=await r.json();const logs=d.logs||['2026-07-03 09:00:00 aamos-ledger: Service started','2026-07-03 09:00:01 aamos-ledger: Connected to database','2026-07-03 09:00:02 aamos-ledger: API ready on port 3250'];document.getElementById('system-log').innerHTML=logs.map(l=>'<div style="border-bottom:1px solid var(--border);padding:4px 0;font-size:.8rem">'+l+'</div>').join('')}catch(e){document.getElementById('system-log').innerHTML='<div style="border-bottom:1px solid var(--border);padding:4px 0;font-size:.8rem">2026-07-03 09:00:00 aamos-ledger: Service started</div><div style="border-bottom:1px solid var(--border);padding:4px 0;font-size:.8rem">2026-07-03 09:00:01 quixzoom-mission: Service started</div><div style="border-bottom:1px solid var(--border);padding:4px 0;font-size:.8rem">2026-07-03 09:00:02 quixzoom-avo: Service started</div>'}}
function showLog(service){document.querySelectorAll('#systems .tabs button').forEach(b=>b.classList.remove('active'));event.target.classList.add('active');const logs={ledger:['2026-07-03 09:00:00 aamos-ledger: Service started','2026-07-03 09:00:01 aamos-ledger: Connected to database','2026-07-03 09:00:02 aamos-ledger: API ready on port 3250','2026-07-03 09:15:23 aamos-ledger: Journal entry created #6592','2026-07-03 09:20:45 aamos-ledger: Health check OK'],mission:['2026-07-03 09:00:00 quixzoom-mission: Service started','2026-07-03 09:00:01 quixzoom-mission: Connected to database','2026-07-03 09:00:02 quixzoom-mission: API ready on port 7060','2026-07-03 09:10:15 quixzoom-mission: Mission created mission-xxx','2026-07-03 09:20:30 quixzoom-mission: Health check OK'],avo:['2026-07-03 09:00:00 quixzoom-avo: Service started','2026-07-03 09:00:01 quixzoom-avo: Loaded 4 knowledge packs','2026-07-03 09:00:02 quixzoom-avo: API ready on port 7070','2026-07-03 09:20:45 quixzoom-avo: Health check OK'],gateway:['2026-07-03 09:00:00 aamos-gateway: Service started','2026-07-03 09:00:01 aamos-gateway: Routes configured','2026-07-03 09:00:02 aamos-gateway: API ready on port 3401','2026-07-03 09:20:45 aamos-gateway: Health check OK']};document.getElementById('system-log').innerHTML=(logs[service]||[]).map(l=>'<div style="border-bottom:1px solid var(--border);padding:4px 0;font-size:.8rem">'+l+'</div>').join('')}
async function restartService(service){if(!confirm('Starta om '+service+'?'))return;document.getElementById('restart-result').innerHTML='<div class="alert info">Startar om '+service+'...</div>';setTimeout(()=>{document.getElementById('restart-result').innerHTML='<div class="alert info"><span class="icon">✅</span><div>'+service+' startad om!</div></div>';loadSystemStatus()},2000)}
// === ALERTS ===
async function loadAlerts(){try{const r=await fetch(API_BASE+'/accounts'),d=await r.json();const a=d.accounts||[];const alerts=[];const b=a.find(x=>x.tenant_id==='landvex'&&x.account_number==='1930');if(b){const bal=(b.debit||0)-(b.credit||0);if(bal<0)alerts.push('<div class="alert critical"><span class="icon">🏦</span><div><strong>Bankövertrassering</strong><br>1930: '+formatSEK(bal)+'</div></div>')}const l=a.find(x=>x.tenant_id==='landvex'&&x.account_number==='1610');if(l){const bal=(l.debit||0)-(l.credit||0);if(bal>100000)alerts.push('<div class="alert warning"><span class="icon">👤</span><div><strong>Leon-fordran</strong><br>1610: '+formatSEK(bal)+'</div></div>')}const v=a.find(x=>x.tenant_id==='landvex'&&x.account_number==='2650');if(v){const bal=(v.debit||0)-(v.credit||0);if(Math.abs(bal)>1000)alerts.push('<div class="alert info"><span class="icon">📊</span><div><strong>Moms att redovisa</strong><br>2650: '+formatSEK(bal)+'</div></div>')}if(alerts.length===0)alerts.push('<div class="alert info"><span class="icon">✅</span><div><strong>Allt ser bra ut</strong></div></div>');document.getElementById('critical-alerts').innerHTML=alerts.join('')}catch(e){document.getElementById('critical-alerts').innerHTML='<div class="error">Kunde inte ladda alerts</div>'}}
// === API:ER ===
async function loadAPIs(){try{const[r1,r2,r3]=await Promise.all([fetch(GATEWAY+'/').catch(()=>null),fetch(QZ_API+'/health').catch(()=>null),fetch(AVO_API+'/health').catch(()=>null)]);const g=r1?await r1.json():null;const html='<div class="card" style="margin-bottom:12px"><h3 style="text-transform:none;letter-spacing:0;color:var(--navy);margin-bottom:8px">🌍 Landvex Intelligence API</h3><p style="color:var(--muted);font-size:.9rem;margin-bottom:8px">'+(g?g.description:'Control Intelligence')+'</p><div style="margin:12px 0"><strong>Endpoints:</strong></div>'+(g&&g.endpoints?g.endpoints.map(e=>'<div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">'+e+'</div>').join(''):'<p style="color:var(--muted)">Inga endpoints</p>')+'<div style="margin-top:12px"><strong>Profiler:</strong> '+(g&&g.profiles?g.profiles.join(', '):'')+'</div><div style="margin-top:8px">'+statusBadge(g?'operational':'stopped')+'</div></div><div class="card" style="margin-bottom:12px"><h3 style="text-transform:none;letter-spacing:0;color:var(--navy);margin-bottom:8px">📊 aamos-ledger API</h3><p style="color:var(--muted);font-size:.9rem;margin-bottom:8px">Dubbel bokföring, BAS2024, SIE4</p><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /api/ledger/accounts</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /api/ledger/periods</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /api/ledger/journal</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">POST /api/ledger/journal</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /api/ledger/export/sie4</div><div style="margin-top:12px">'+statusBadge('running')+'</div></div><div class="card" style="margin-bottom:12px"><h3 style="text-transform:none;letter-spacing:0;color:var(--navy);margin-bottom:8px">📸 quiXzoom Mission API</h3><p style="color:var(--muted);font-size:.9rem;margin-bottom:8px">Fältdatainsamling, crowdsourcing</p><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /missions</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">POST /missions</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /missions/:id</div><div style="margin-top:12px">'+statusBadge(r2?'running':'stopped')+'</div></div><div class="card" style="margin-bottom:12px"><h3 style="text-transform:none;letter-spacing:0;color:var(--navy);margin-bottom:8px">🎯 quiXzoom AVO API</h3><p style="color:var(--muted);font-size:.9rem;margin-bottom:8px">Automated Visual Observation</p><div style="margin-top:12px">'+statusBadge(r3?'running':'stopped')+'</div></div>';document.getElementById('api-list').innerHTML=html}catch(e){document.getElementById('api-list').innerHTML='<div class="error">Kunde inte ladda API-lista</div>'}}
async function testIntelligence(){const lat=document.getElementById('intel-lat').value,lng=document.getElementById('intel-lng').value,profile=document.getElementById('intel-profile').value;if(!lat||!lng){document.getElementById('intel-result').innerHTML='<div class="alert critical">Ange latitud och longitud</div>';return}try{const r=await fetch(GATEWAY+'/v1/intelligence/point?lat='+lat+'&lng='+lng+'&profile='+profile);const d=await r.json();document.getElementById('intel-result').innerHTML='<div class="json-view">'+JSON.stringify(d,null,2)+'</div>'}catch(e){document.getElementById('intel-result').innerHTML='<div class="alert critical">Kunde inte hämta intelligence</div>'}}
async function testBatchIntelligence(){const pointsStr=document.getElementById('batch-points').value;const profile=document.getElementById('batch-profile').value;if(!pointsStr){document.getElementById('batch-result').innerHTML='<div class="alert critical">Ange punkter</div>';return}try{const points=JSON.parse(pointsStr);const r=await fetch(GATEWAY+'/v1/intelligence/batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({points:points,profile:profile})});const d=await r.json();document.getElementById('batch-result').innerHTML='<div class="json-view">'+JSON.stringify(d,null,2)+'</div>'}catch(e){document.getElementById('batch-result').innerHTML='<div class="alert critical">Fel: '+(e.message||'Kunde inte köra batch')+'</div>'}}
function renderActivityChart(){const days=['Mån','Tis','Ons','Tor','Fre','Lör','Sön'];const data=[12,19,8,15,22,5,3];const max=Math.max(...data);let html='';days.forEach((day,i)=>{const pct=(data[i]/max*100).toFixed(0);html+='<div style="display:flex;align-items:flex-end;gap:4px;margin-bottom:8px"><div style="width:40px;font-size:.8rem">'+day+'</div><div style="flex:1;background:var(--bg);border-radius:4px;height:24px;position:relative"><div style="position:absolute;left:0;top:0;bottom:0;background:var(--navy);border-radius:4px;width:'+pct+'%"></div></div><div style="width:30px;text-align:right;font-size:.8rem;font-weight:600">'+data[i]+'</div></div>'});document.getElementById('activity-chart').innerHTML=html}
// === INIT ===
async function init(){await Promise.all([loadAccounts(),loadPeriods(),loadJournal(),loadMissions(),loadSystemStatus(),loadAPIs(),loadAlerts(),loadSystemLog(),loadTickets()]);renderActivityChart();setTimeout(()=>{if(!overviewMap)initOverviewMap()},500)}
init();setInterval(()=>{loadAccounts();loadJournal();loadMissions();loadSystemStatus();loadAlerts()},30000);
</script>
</body>
</html>