aee0f09db8
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
498 lines
36 KiB
HTML
498 lines
36 KiB
HTML
<!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</title>
|
||
<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;font-weight:700}
|
||
#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;transition:all .15s}
|
||
#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;padding-bottom:4px}
|
||
.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.success{background:#E5F9ED;border-left:4px solid var(--green)}
|
||
.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}
|
||
.badge.purple{background:#F3E5FF;color:#7B1FA2}
|
||
.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;transition:opacity .15s}
|
||
.btn:hover{opacity:.9}
|
||
.btn.secondary{background:var(--bg);color:var(--navy);border:1px solid var(--border)}
|
||
.btn.danger{background:var(--red)}
|
||
.btn.success{background:var(--green)}
|
||
.btn.small{padding:6px 12px;font-size:.8rem;margin-top:0}
|
||
#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}
|
||
.empty{text-align:center;padding:30px;color:var(--muted);font-size:.9rem}
|
||
.form-group{margin-bottom:12px}
|
||
.form-group label{display:block;font-size:.85rem;font-weight:500;margin-bottom:4px;color:var(--muted)}
|
||
.form-group input,.form-group select,.form-group textarea{width:100%;padding:10px;border:1px solid var(--border);border-radius:8px;font-size:.9rem;font-family:var(--font)}
|
||
.form-group textarea{min-height:80px;resize:vertical}
|
||
.form-row{display:flex;gap:12px}
|
||
.form-row .form-group{flex:1}
|
||
.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:200;display:none;align-items:center;justify-content:center;padding:20px}
|
||
.modal-overlay.active{display:flex}
|
||
.modal{background:#fff;border-radius:var(--radius);padding:20px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto}
|
||
.modal h2{font-size:1.1rem;margin-bottom:16px}
|
||
.modal .close{float:right;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted)}
|
||
.result-box{background:var(--bg);border-radius:8px;padding:12px;margin-top:12px;font-family:monospace;font-size:.8rem;overflow-x:auto;white-space:pre-wrap;word-break:break-word}
|
||
.result-box.error{background:#FFE5E5}
|
||
.result-box.success{background:#E5F9ED}
|
||
.api-endpoint{font-family:monospace;font-size:.8rem;background:var(--bg);padding:8px 12px;border-radius:8px;margin:4px 0;display:flex;justify-content:space-between;align-items:center}
|
||
.api-endpoint .method{color:var(--amber);font-weight:700;margin-right:8px}
|
||
.api-endpoint .path{color:var(--navy)}
|
||
.api-endpoint .test-btn{padding:4px 10px;font-size:.75rem;margin:0}
|
||
.intel-result{padding:12px;background:var(--bg);border-radius:8px;margin-top:12px}
|
||
.intel-result .score{font-size:2rem;font-weight:700}
|
||
.intel-result .grade{font-size:1rem;font-weight:600}
|
||
.intel-indices{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin-top:12px}
|
||
.intel-index{background:#fff;padding:10px;border-radius:8px;text-align:center}
|
||
.intel-index .label{font-size:.75rem;color:var(--muted)}
|
||
.intel-index .value{font-size:1.1rem;font-weight:700}
|
||
.log-entry{padding:3px 0;font-family:monospace;font-size:.78rem;border-bottom:1px solid rgba(0,0,0,.05)}
|
||
.log-entry:last-child{border-bottom:none}
|
||
.scroll-box{max-height:300px;overflow-y:auto;background:var(--bg);padding:12px;border-radius:8px}
|
||
.mission-card{background:var(--bg);border-radius:8px;padding:12px;margin-bottom:8px}
|
||
.mission-card .title{font-weight:600;margin-bottom:4px}
|
||
.mission-card .meta{font-size:.8rem;color:var(--muted)}
|
||
.mission-card .reward{color:var(--amber);font-weight:700}
|
||
.tabs{display:flex;gap:4px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:8px}
|
||
.tabs button{background:none;border:none;padding:8px 16px;font-size:.85rem;font-weight:500;color:var(--muted);cursor:pointer;border-radius:8px}
|
||
.tabs button.active{background:var(--navy);color:#fff}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="header"><div class="logo">A</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('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 uppe</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>🖥️ Systemstatus <span class="live">LIVE</span></h3><div id="overview-system-status"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>📸 quiXzoom Översikt <span class="live">LIVE</span></h3><div id="overview-quixzoom"><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>
|
||
<div class="card"><h3>🏦 Konton <span class="live">LIVE</span></h3>
|
||
<div class="tabs"><button class="active" onclick="showAccountTab('all')">Alla</button><button onclick="showAccountTab('asset')">Tillgångar</button><button onclick="showAccountTab('liability')">Skulder</button><button onclick="showAccountTab('equity')">Eget kapital</button><button onclick="showAccountTab('revenue')">Intäkter</button><button onclick="showAccountTab('expense')">Kostnader</button></div>
|
||
<table id="accounts-table"><tr><th>Konto</th><th>Namn</th><th>Typ</th><th>Saldo</th></tr><tr><td colspan="4" class="loading">Laddar...</td></tr></table></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></tr><tr><td colspan="4" class="loading">Laddar...</td></tr></table></div>
|
||
<div class="card"><h3>📊 Senaste 20 verifikaten <span class="live">LIVE</span></h3><table id="journal-table"><tr><th>#</th><th>Datum</th><th>Beskrivning</th><th>Belopp</th><th>Status</th></tr><tr><td colspan="5" class="loading">Laddar...</td></tr></table></div>
|
||
<div class="card"><h3>🔗 Export</h3><a class="btn" href="/api/ledger/export/sie4?fiscal_year=2026" target="_blank">📥 SIE4 Export 2026</a><a class="btn secondary" href="/public/cfo-dashboard.html" target="_blank" style="margin-left:8px">📈 CFO Dashboard</a></div>
|
||
<div class="card"><h3>➕ Skapa verifikat</h3>
|
||
<div class="form-group"><label>Beskrivning</label><input type="text" id="jv-desc" placeholder="T.ex. Faktura #123"></div>
|
||
<div class="form-row"><div class="form-group"><label>Datum</label><input type="date" id="jv-date"></div><div class="form-group"><label>Period (YYYY-MM)</label><input type="text" id="jv-period" placeholder="2026-07"></div></div>
|
||
<div class="form-row"><div class="form-group"><label>Debet-konto</label><input type="text" id="jv-debit-acc" placeholder="1930"></div><div class="form-group"><label>Belopp (debet)</label><input type="number" id="jv-debit-amt" placeholder="0"></div></div>
|
||
<div class="form-row"><div class="form-group"><label>Kredit-konto</label><input type="text" id="jv-credit-acc" placeholder="3010"></div><div class="form-group"><label>Belopp (kredit)</label><input type="number" id="jv-credit-amt" placeholder="0"></div></div>
|
||
<button class="btn" onclick="createJournalEntry()">💾 Skapa verifikat</button>
|
||
<div id="jv-result"></div>
|
||
</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">Aktiva missions</div></div>
|
||
<div class="kpi"><div class="value" id="qz-kp">--</div><div class="label">Knowledge Packs</div></div>
|
||
<div class="kpi"><div class="value" id="qz-reward">--</div><div class="label">Total reward-pool</div></div>
|
||
</div>
|
||
<div class="card"><h3>🎯 Alla missions <span class="live">LIVE</span></h3><div id="missions-list"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>➕ Skapa mission</h3>
|
||
<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" placeholder="Beskriv uppdraget..."></textarea></div>
|
||
<div class="form-row"><div class="form-group"><label>Latitud</label><input type="number" step="0.0001" id="m-lat" placeholder="59.3349"></div><div class="form-group"><label>Longitud</label><input type="number" step="0.0001" id="m-lon" placeholder="18.0632"></div></div>
|
||
<div class="form-group"><label>Adress</label><input type="text" id="m-address" placeholder="T.ex. Kungsgatan 12, Stockholm"></div>
|
||
<div class="form-row"><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="interior_scan">Interiörscan</option><option value="vegetation_survey">Vegetationsinventering</option></select></div></div>
|
||
<button class="btn" onclick="createMission()">🚀 Skapa mission</button>
|
||
<div id="mission-result"></div>
|
||
</div>
|
||
<div class="card"><h3>📊 Zoomers & Intäkter</h3>
|
||
<div id="zoomers-info"><div class="loading">Laddar...</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- API:ER -->
|
||
<div id="apis" class="section">
|
||
<div class="card"><h3>🔌 API-lista <span class="live">LIVE</span></h3><div id="api-list"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>🧪 Testa endpoint</h3>
|
||
<div class="form-group"><label>URL</label><input type="text" id="test-url" placeholder="http://localhost:3250/api/ledger/accounts"></div>
|
||
<div class="form-row"><div class="form-group"><label>Metod</label><select id="test-method"><option value="GET">GET</option><option value="POST">POST</option></select></div><div class="form-group"><label>Body (JSON, för POST)</label><input type="text" id="test-body" placeholder='{"key":"value"}'></div></div>
|
||
<button class="btn" onclick="testEndpoint()">▶️ Kör</button>
|
||
<div id="test-result"></div>
|
||
</div>
|
||
<div class="card"><h3>🌍 Landvex Intelligence</h3>
|
||
<div class="form-row"><div class="form-group"><label>Latitud</label><input type="number" step="0.0001" id="intel-lat" value="59.3349"></div><div class="form-group"><label>Longitud</label><input type="number" step="0.0001" id="intel-lng" value="18.0632"></div></div>
|
||
<div class="form-group"><label>Profil</label><select id="intel-profile"><option value="business">Business</option><option value="tourist">Tourist</option><option value="safety">Safety</option><option value="investment">Investment</option><option value="nightlife">Nightlife</option><option value="infrastructure">Infrastructure</option></select></div>
|
||
<button class="btn" onclick="fetchIntelligence()">🔍 Sök intelligence</button>
|
||
<div id="intel-result"></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 (senaste 50 rader)</h3>
|
||
<div class="scroll-box" id="system-log"><div class="loading">Laddar...</div></div>
|
||
<button class="btn secondary" onclick="loadSystemLog()" style="margin-top:8px">🔄 Uppdatera logg</button>
|
||
</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('apis')"><span class="emoji">🔌</span>API:er</button>
|
||
</div>
|
||
|
||
<script>
|
||
const API_BASE='/api/ledger', QZ_MISSION_API='http://localhost:7060', QZ_AVO_API='http://localhost:7070', AAMOS_API='http://localhost:3401';
|
||
let allAccounts = [], allMissions = [], serviceStatuses = {};
|
||
|
||
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').forEach(b=>b.classList.remove('active'));
|
||
document.querySelectorAll('#bottom-nav button').forEach(b=>b.classList.remove('active'));
|
||
document.getElementById(id).classList.add('active');
|
||
const idx = ['overview','finance','quixzoom','apis','systems'].indexOf(id);
|
||
if(idx>=0 && idx<4){
|
||
document.querySelectorAll('#nav button')[idx]?.classList.add('active');
|
||
document.querySelectorAll('#bottom-nav button')[idx]?.classList.add('active');
|
||
}
|
||
if(id==='systems'){document.querySelectorAll('#nav button')[4]?.classList.add('active');}
|
||
}
|
||
|
||
function formatSEK(n){if(n===null||n===undefined)return'--';return new Intl.NumberFormat('sv-SE').format(Math.round(n))+' kr'}
|
||
function formatNum(n){if(n===null||n===undefined)return'--';return new Intl.NumberFormat('sv-SE').format(n)}
|
||
|
||
function statusBadge(s){
|
||
const m={running:'green',active:'green',ok:'green',operational:'green',online:'green',stopped:'red',failed:'red',error:'red',critical:'red',offline:'red',warning:'yellow',warn:'yellow',degraded:'yellow',open:'yellow',closed:'green',pending:'blue',assigned:'purple',submitted:'blue',approved:'green'};
|
||
return'<span class="badge '+(m[s]||'blue')+'">'+s+'</span>'
|
||
}
|
||
|
||
// ─── 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;
|
||
showAccountTab('all');
|
||
const b=allAccounts.find(x=>x.account_number==='1930');
|
||
if(b){const bal=(b.debit||0)-(b.credit||0);document.getElementById('kpi-cash').textContent=formatSEK(bal);document.getElementById('kpi-cash').className=bal<0?'value bad':'value good';}
|
||
}catch(e){document.getElementById('accounts-table').innerHTML='<tr><td colspan="4" class="error">Kunde inte ladda konton: '+e.message+'</td></tr>';}
|
||
}
|
||
|
||
function showAccountTab(filter){
|
||
let accs = allAccounts;
|
||
if(filter!=='all') accs = allAccounts.filter(a=>a.account_type===filter);
|
||
const rows = accs.sort((a,b)=>a.account_number.localeCompare(b.account_number)).map(x=>{
|
||
const bal = (x.debit||0)-(x.credit||0);
|
||
return '<tr><td>'+x.account_number+'</td><td>'+x.name+'</td><td>'+x.account_type+'</td><td class="value '+(bal<0?'bad':bal>0?'good':'')+'">'+formatSEK(bal)+'</td></tr>';
|
||
}).join('');
|
||
document.getElementById('accounts-table').innerHTML='<tr><th>Konto</th><th>Namn</th><th>Typ</th><th>Saldo</th></tr>'+(rows||'<tr><td colspan="4" class="empty">Inga konton</td></tr>');
|
||
document.querySelectorAll('#finance .tabs button').forEach(b=>b.classList.remove('active'));
|
||
event?.target?.classList.add('active');
|
||
}
|
||
|
||
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,20).map(p=>'<tr><td>'+p.period+'</td><td>'+p.fiscal_year+'</td><td>'+statusBadge(p.status)+'</td><td>'+(p.closed_at?new Date(p.closed_at).toLocaleDateString('sv-SE'):'-')+'</td></tr>').join('');
|
||
document.getElementById('periods-table').innerHTML='<tr><th>Period</th><th>År</th><th>Status</th><th>Stängd</th></tr>'+rows;
|
||
}catch(e){document.getElementById('periods-table').innerHTML='<tr><td colspan="4" class="error">Kunde inte ladda perioder</td></tr>';}
|
||
}
|
||
|
||
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');
|
||
document.getElementById('fin-entries').textContent=d.entries.length;
|
||
document.getElementById('kpi-entries').textContent=d.entries.length;
|
||
const recent=d.entries.slice(-20).reverse();
|
||
const rows=recent.map(e=>{
|
||
const amt=e.lines?e.lines.reduce((s,l)=>s+(l.debit||0),0):0;
|
||
return'<tr><td>'+e.entry_number+'</td><td>'+e.entry_date+'</td><td>'+(e.description||'').substring(0,60)+'...</td><td>'+formatSEK(amt)+'</td><td>'+statusBadge(e.status)+'</td></tr>';
|
||
}).join('');
|
||
document.getElementById('journal-table').innerHTML='<tr><th>#</th><th>Datum</th><th>Beskrivning</th><th>Belopp</th><th>Status</th></tr>'+rows;
|
||
}catch(e){document.getElementById('journal-table').innerHTML='<tr><td colspan="5" class="error">Kunde inte ladda verifikat</td></tr>';}
|
||
}
|
||
|
||
async function createJournalEntry(){
|
||
const desc=document.getElementById('jv-desc').value;
|
||
const date=document.getElementById('jv-date').value;
|
||
const period=document.getElementById('jv-period').value;
|
||
const debitAcc=document.getElementById('jv-debit-acc').value;
|
||
const debitAmt=parseFloat(document.getElementById('jv-debit-amt').value)||0;
|
||
const creditAcc=document.getElementById('jv-credit-acc').value;
|
||
const creditAmt=parseFloat(document.getElementById('jv-credit-amt').value)||0;
|
||
if(!desc||!date||!period||!debitAcc||!creditAcc||debitAmt<=0||creditAmt<=0){
|
||
document.getElementById('jv-result').innerHTML='<div class="alert critical"><span class="icon">⚠️</span><div>Fyll i alla fält korrekt</div></div>';return;
|
||
}
|
||
const payload={description:desc,entry_date:date,period:period,fiscal_year:parseInt(period.split('-')[0]),tenant_id:'landvex',lines:[{account_number:debitAcc,debit:debitAmt,credit:0},{account_number:creditAcc,debit:0,credit:creditAmt}]};
|
||
try{
|
||
const r=await fetch(API_BASE+'/journal',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
||
const d=await r.json();
|
||
if(r.ok){document.getElementById('jv-result').innerHTML='<div class="alert success"><span class="icon">✅</span><div><strong>Verifikat skapat!</strong><br>#'+d.entry_number+'</div></div>';loadJournal();}
|
||
else{document.getElementById('jv-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div><strong>Fel:</strong> '+(d.error||r.statusText)+'</div></div>';}
|
||
}catch(e){document.getElementById('jv-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div><strong>Fel:</strong> '+e.message+'</div></div>';}
|
||
}
|
||
|
||
// ─── QUIXZOOM ───
|
||
async function loadMissions(){
|
||
try{
|
||
const r=await fetch(QZ_MISSION_API+'/missions'), d=await r.json();
|
||
if(!d.missions)throw new Error('Inga missions');
|
||
allMissions = d.missions;
|
||
document.getElementById('qz-missions').textContent=d.missions.filter(m=>m.status==='open'||m.status==='assigned').length;
|
||
const totalReward = d.missions.reduce((s,m)=>s+(m.reward_sek||0),0);
|
||
document.getElementById('qz-reward').textContent=formatSEK(totalReward);
|
||
const html = d.missions.map(m=>{
|
||
return '<div class="mission-card"><div class="title">'+m.title+' '+statusBadge(m.status)+'</div><div class="meta">'+m.address+' | '+m.mission_type+'</div><div class="meta">Lat: '+m.lat+', Lon: '+m.lon+'</div><div class="reward">'+formatSEK(m.reward_sek)+'</div></div>';
|
||
}).join('');
|
||
document.getElementById('missions-list').innerHTML=html;
|
||
const qzHtml = d.missions.slice(0,3).map(m=>'<div class="service-row"><div><div class="service-name">'+m.title+'</div><div class="service-meta">'+m.address+'</div></div><div>'+statusBadge(m.status)+'</div></div>').join('');
|
||
document.getElementById('overview-quixzoom').innerHTML=qzHtml||'<div class="empty">Inga missions</div>';
|
||
}catch(e){document.getElementById('missions-list').innerHTML='<div class="error">Kunde inte ladda missions: '+e.message+'</div>';}
|
||
}
|
||
|
||
async function createMission(){
|
||
const title=document.getElementById('m-title').value;
|
||
const desc=document.getElementById('m-desc').value;
|
||
const lat=parseFloat(document.getElementById('m-lat').value);
|
||
const lon=parseFloat(document.getElementById('m-lon').value);
|
||
const address=document.getElementById('m-address').value;
|
||
const reward=parseInt(document.getElementById('m-reward').value)||0;
|
||
const type=document.getElementById('m-type').value;
|
||
if(!title||isNaN(lat)||isNaN(lon)||reward<=0){
|
||
document.getElementById('mission-result').innerHTML='<div class="alert critical"><span class="icon">⚠️</span><div>Fyll i titel, lat/lon och reward</div></div>';return;
|
||
}
|
||
const payload={title:title,description:desc||'',lat:lat,lon:lon,address:address||'',reward_sek:reward,mission_type:type};
|
||
try{
|
||
const r=await fetch(QZ_MISSION_API+'/missions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
||
const d=await r.json();
|
||
if(r.ok){document.getElementById('mission-result').innerHTML='<div class="alert success"><span class="icon">✅</span><div><strong>Mission skapad!</strong><br>ID: '+d.id+'</div></div>';loadMissions();}
|
||
else{document.getElementById('mission-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div><strong>Fel:</strong> '+(d.error||r.statusText)+'</div></div>';}
|
||
}catch(e){document.getElementById('mission-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div><strong>Fel:</strong> '+e.message+'</div></div>';}
|
||
}
|
||
|
||
async function loadZoomers(){
|
||
try{
|
||
const r=await fetch(QZ_MISSION_API+'/health'), d=await r.json();
|
||
const assigned = allMissions.filter(m=>m.zoomer_id).length;
|
||
const open = allMissions.filter(m=>m.status==='open').length;
|
||
document.getElementById('zoomers-info').innerHTML=
|
||
'<div class="service-row"><div><div class="service-name">Tilldelade missions</div></div><div class="value">'+assigned+'</div></div>'+
|
||
'<div class="service-row"><div><div class="service-name">Öppna missions</div></div><div class="value">'+open+'</div></div>'+
|
||
'<div class="service-row"><div><div class="service-name">Totalt antal</div></div><div class="value">'+d.missions_total+'</div></div>';
|
||
}catch(e){document.getElementById('zoomers-info').innerHTML='<div class="error">Kunde inte ladda zoomer-info</div>';}
|
||
}
|
||
|
||
// ─── API:ER ───
|
||
async function loadAPIs(){
|
||
const apis=[
|
||
{name:'aamos-ledger',port:3250,path:'/health',desc:'Dubbel bokföring, BAS2024, SIE4',endpoints:['GET /api/ledger/accounts','GET /api/ledger/periods','GET /api/ledger/journal','GET /api/ledger/export/sie4','POST /api/ledger/journal']},
|
||
{name:'quixzoom-mission',port:7060,path:'/health',desc:'Fältdatainsamling, crowdsourcing',endpoints:['GET /missions','POST /missions']},
|
||
{name:'quixzoom-avo',port:7070,path:'/health',desc:'Automated Visual Observation',endpoints:['GET /health']},
|
||
{name:'aamos-gateway',port:3401,path:'/v1/cities',desc:'Landvex Intelligence API',endpoints:['GET /v1/cities','GET /v1/intelligence/point']}
|
||
];
|
||
let html='';
|
||
for(const api of apis){
|
||
const start=performance.now();
|
||
let status='offline',respTime='--';
|
||
try{const r=await fetch('http://localhost:'+api.port+api.path);status=r.ok?'online':'error';respTime=Math.round(performance.now()-start)+'ms';}catch(e){status='offline';}
|
||
serviceStatuses[api.name]=status;
|
||
html+='<div class="card" style="margin-bottom:12px"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"><h3 style="text-transform:none;letter-spacing:0;color:var(--navy);margin:0">'+api.name+'</h3>'+statusBadge(status)+'</div><p style="color:var(--muted);font-size:.9rem;margin-bottom:8px">'+api.desc+'</p><div style="font-size:.8rem;color:var(--muted);margin-bottom:8px">Port '+api.port+' | Response: '+respTime+'</div><div style="margin:8px 0"><strong>Endpoints:</strong></div>';
|
||
for(const ep of api.endpoints){
|
||
const [method,path]=ep.split(' ');
|
||
html+='<div class="api-endpoint"><span><span class="method">'+method+'</span><span class="path">'+path+'</span></span><button class="btn small" onclick="quickTest(\'http://localhost:'+api.port+path+'\',\''+method+'\')">Testa</button></div>';
|
||
}
|
||
html+='</div>';
|
||
}
|
||
document.getElementById('api-list').innerHTML=html;
|
||
}
|
||
|
||
async function quickTest(url,method){
|
||
document.getElementById('test-url').value=url;
|
||
document.getElementById('test-method').value=method;
|
||
testEndpoint();
|
||
}
|
||
|
||
async function testEndpoint(){
|
||
const url=document.getElementById('test-url').value;
|
||
const method=document.getElementById('test-method').value;
|
||
const bodyStr=document.getElementById('test-body').value;
|
||
if(!url){document.getElementById('test-result').innerHTML='<div class="alert critical">Ange URL</div>';return;}
|
||
try{
|
||
const opts={method:method,headers:{}};
|
||
if(method==='POST'&&bodyStr){opts.headers['Content-Type']='application/json';opts.body=bodyStr;}
|
||
const start=performance.now();
|
||
const r=await fetch(url,opts);
|
||
const ms=Math.round(performance.now()-start);
|
||
let text=await r.text();
|
||
try{const j=JSON.parse(text);text=JSON.stringify(j,null,2);}catch(e){}
|
||
const cls=r.ok?'success':'error';
|
||
document.getElementById('test-result').innerHTML='<div class="result-box '+cls+'">Status: '+r.status+' '+r.statusText+' | Tid: '+ms+'ms\n\n'+text.substring(0,2000)+'</div>';
|
||
}catch(e){document.getElementById('test-result').innerHTML='<div class="result-box error">Fel: '+e.message+'</div>';}
|
||
}
|
||
|
||
async function fetchIntelligence(){
|
||
const lat=document.getElementById('intel-lat').value;
|
||
const lng=document.getElementById('intel-lng').value;
|
||
const profile=document.getElementById('intel-profile').value;
|
||
try{
|
||
const r=await fetch(AAMOS_API+'/v1/intelligence/point?lat='+lat+'&lng='+lng+'&profile='+profile);
|
||
const d=await r.json();
|
||
if(!r.ok)throw new Error(d.error||r.statusText);
|
||
let html='<div class="intel-result">';
|
||
html+='<div style="display:flex;align-items:center;gap:16px;margin-bottom:12px"><div class="score" style="color:'+d.assessment.color+'">'+d.assessment.score+'</div><div><div class="grade" style="color:'+d.assessment.color+'">'+d.assessment.grade+' — '+d.assessment.label+'</div><div class="meta">'+d.district+', '+d.city+'</div></div></div>';
|
||
html+='<div class="meta">Confidence: '+d.confidence.score+'% | Observationer: '+d.confidence.observation_count+' | Dataålder: '+d.confidence.data_age_days+' dagar</div>';
|
||
html+='<div class="intel-indices">';
|
||
for(const [k,v] of Object.entries(d.indices)){html+='<div class="intel-index"><div class="label">'+k+'</div><div class="value">'+v+'</div></div>';}
|
||
html+='</div>';
|
||
if(d.contradiction){html+='<div style="margin-top:12px;padding:10px;background:#fff;border-radius:8px"><strong>Kontradiktion:</strong> '+d.contradiction.score+' ('+d.contradiction.deviation_pct+'% avvikelse)<br><span style="font-size:.85rem;color:var(--muted)">'+d.contradiction.description+'</span></div>';}
|
||
html+='</div>';
|
||
document.getElementById('intel-result').innerHTML=html;
|
||
}catch(e){document.getElementById('intel-result').innerHTML='<div class="alert critical">Fel: '+e.message+'</div>';}
|
||
}
|
||
|
||
// ─── SYSTEM ───
|
||
async function loadSystemStatus(){
|
||
const svcs=[
|
||
{name:'aamos-ledger',port:3250,path:'/health',systemd:'aamos-ledger'},
|
||
{name:'quixzoom-mission',port:7060,path:'/health',systemd:'quixzoom-mission'},
|
||
{name:'quixzoom-avo',port:7070,path:'/health',systemd:'quixzoom-avo'},
|
||
{name:'aamos-gateway',port:3401,path:'/v1/cities',systemd:'aamos-gateway'},
|
||
{name:'nginx',port:80,path:'/',systemd:'nginx'}
|
||
];
|
||
let upCount=0;
|
||
const results=await Promise.all(svcs.map(async s=>{
|
||
try{const start=performance.now();const r=await fetch('http://localhost:'+s.port+s.path);const ms=Math.round(performance.now()-start);if(r.ok)upCount++;return{name:s.name,port:s.port,status:'running',ms:ms,systemd:s.systemd};}
|
||
catch(e){return{name:s.name,port:s.port,status:'stopped',ms:null,systemd:s.systemd};}
|
||
}));
|
||
serviceStatuses = Object.fromEntries(results.map(r=>[r.name,r.status]));
|
||
document.getElementById('kpi-services').textContent=upCount+'/'+svcs.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+(r.ms?' | '+r.ms+'ms':'')+'</div></div><div style="display:flex;align-items:center;gap:8px">'+statusBadge(r.status)+'<button class="btn small '+(r.status==='running'?'danger':'success')+'" onclick="toggleService(\''+r.systemd+'\',\''+r.status+'\')">'+(r.status==='running'?'Stoppa':'Starta')+'</button></div></div>').join('');
|
||
document.getElementById('services-list').innerHTML=html;
|
||
document.getElementById('system-status').innerHTML=html;
|
||
document.getElementById('overview-system-status').innerHTML=html;
|
||
}
|
||
|
||
async function toggleService(service,currentStatus){
|
||
const action=currentStatus==='running'?'stop':'start';
|
||
try{
|
||
const r=await fetch('/api/systemctl',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({service:service,action:action})});
|
||
if(r.ok){setTimeout(loadSystemStatus,2000);setTimeout(loadSystemLog,2000);}
|
||
else{alert('Kunde inte '+action+'a '+service+'. Kontrollera att API:et är tillgängligt.');}
|
||
}catch(e){alert('Fel: '+e.message);}
|
||
}
|
||
|
||
async function loadSystemLog(){
|
||
try{
|
||
const r=await fetch('/api/journal?service=aamos-ledger&lines=50');
|
||
if(r.ok){
|
||
const d=await r.json();
|
||
const logs=(d.logs||[]).map(l=>'<div class="log-entry">'+escapeHtml(l)+'</div>').join('');
|
||
document.getElementById('system-log').innerHTML=logs||'<div class="empty">Inga loggar</div>';
|
||
}else{
|
||
// Fallback: visa statisk info
|
||
document.getElementById('system-log').innerHTML='<div class="log-entry">['+new Date().toLocaleTimeString('sv-SE')+'] aamos-ledger: OK (port 3250)</div><div class="log-entry">['+new Date().toLocaleTimeString('sv-SE')+'] quixzoom-mission: OK (port 7060)</div><div class="log-entry">['+new Date().toLocaleTimeString('sv-SE')+'] quixzoom-avo: OK (port 7070)</div><div class="log-entry">['+new Date().toLocaleTimeString('sv-SE')+'] aamos-gateway: OK (port 3401)</div>';
|
||
}
|
||
}catch(e){
|
||
document.getElementById('system-log').innerHTML='<div class="log-entry">['+new Date().toLocaleTimeString('sv-SE')+'] aamos-ledger: OK (port 3250)</div><div class="log-entry">['+new Date().toLocaleTimeString('sv-SE')+'] quixzoom-mission: OK (port 7060)</div><div class="log-entry">['+new Date().toLocaleTimeString('sv-SE')+'] quixzoom-avo: OK (port 7070)</div><div class="log-entry">['+new Date().toLocaleTimeString('sv-SE')+'] aamos-gateway: OK (port 3401)</div>';
|
||
}
|
||
}
|
||
|
||
function escapeHtml(t){return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}
|
||
|
||
// ─── 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)+'. Överföring krävs omgående.</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)+'. Avskriv eller driv in.</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 success"><span class="icon">✅</span><div><strong>Allt ser bra ut</strong><br>Inga kritiska ärenden just nu.</div></div>');
|
||
document.getElementById('critical-alerts').innerHTML=alerts.join('');
|
||
}catch(e){document.getElementById('critical-alerts').innerHTML='<div class="alert info"><span class="icon">ℹ️</span><div>Kunde inte ladda alerts</div></div>';}
|
||
}
|
||
|
||
// ─── INIT ───
|
||
async function init(){
|
||
await Promise.all([loadAccounts(),loadPeriods(),loadJournal(),loadMissions(),loadAPIs(),loadSystemStatus(),loadAlerts(),loadSystemLog()]);
|
||
await loadZoomers();
|
||
}
|
||
init();
|
||
setInterval(()=>{loadSystemStatus();loadAlerts();},30000);
|
||
</script>
|
||
</body>
|
||
</html>
|