6989a98d75
- Arkitektur: docs/auth/passwordless-architecture.md - Backend: iom/quixzoom-auth-service/ (FastAPI + Redis) - Webb: quixzoom-market-pages/se/login/ (QR-kod + polling) - App: iom/quixzoom-app/src/features/auth/ (push + deep links) Flöde: QR-kod → app-godkännande → webb-inloggad
510 lines
19 KiB
HTML
510 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="sv">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>AAMOS Admin — MASTER SYSTEM</title>
|
|
<style>
|
|
:root {
|
|
--primary: #0A9396;
|
|
--danger: #E63946;
|
|
--warning: #F4A261;
|
|
--success: #2A9D8F;
|
|
--bg: #0f1117;
|
|
--card: #1a1d27;
|
|
--border: #2a2d3d;
|
|
--text: #e0e0e0;
|
|
--text-muted: #888;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; }
|
|
.header { background: var(--card); border-bottom: 1px solid var(--border); padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 100; }
|
|
.header h1 { font-size: 1.25rem; font-weight: 600; }
|
|
.header .status { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; }
|
|
.header .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); animation: pulse 2s infinite; }
|
|
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
|
|
.nav { background: var(--card); border-right: 1px solid var(--border); width: 260px; position: fixed; height: 100vh; padding: 1.5rem; overflow-y: auto; }
|
|
.nav h2 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); margin: 1.5rem 0 0.5rem; }
|
|
.nav h2:first-child { margin-top: 0; }
|
|
.nav a { display: block; color: var(--text-muted); text-decoration: none; padding: 0.5rem 0.75rem; border-radius: 6px; margin-bottom: 2px; transition: all 0.2s; font-size: 0.875rem; cursor: pointer; }
|
|
.nav a:hover, .nav a.active { color: var(--text); background: rgba(255,255,255,0.05); }
|
|
.main { margin-left: 260px; padding: 2rem; }
|
|
.alert { background: rgba(230, 57, 70, 0.1); border: 1px solid rgba(230, 57, 70, 0.3); border-left: 4px solid var(--danger); padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; }
|
|
.alert.warning { background: rgba(244, 162, 97, 0.1); border-color: rgba(244, 162, 97, 0.3); border-left-color: var(--warning); }
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; }
|
|
.card h3 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 0.5rem; }
|
|
.card .value { font-size: 2.5rem; font-weight: 700; color: var(--primary); }
|
|
.card .value.danger { color: var(--danger); }
|
|
.card .value.warning { color: var(--warning); }
|
|
.card .subtitle { font-size: 0.875rem; color: var(--text-muted); margin-top: 0.25rem; }
|
|
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
|
th, td { text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--border); }
|
|
th { font-weight: 600; color: var(--text-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
tr:hover { background: rgba(255,255,255,0.02); }
|
|
.badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
|
|
.badge-success { background: rgba(42, 157, 143, 0.2); color: var(--success); }
|
|
.badge-warning { background: rgba(244, 162, 97, 0.2); color: var(--warning); }
|
|
.badge-danger { background: rgba(230, 57, 70, 0.2); color: var(--danger); }
|
|
.badge-blue { background: rgba(0, 122, 255, 0.2); color: #007AFF; }
|
|
.loading { color: var(--text-muted); font-style: italic; }
|
|
.error { color: var(--danger); }
|
|
.hidden { display: none; }
|
|
.section-title { font-size: 1.25rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
.mail-unread { background: rgba(0, 122, 255, 0.1); border-left: 4px solid #007AFF; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="header">
|
|
<div>
|
|
<h1>🔁 AAMOS Admin — MASTER SYSTEM</h1>
|
|
</div>
|
|
<div class="status">
|
|
<span class="status-dot"></span>
|
|
<span id="system-status">LIVE DATA</span>
|
|
</div>
|
|
</header>
|
|
|
|
<nav class="nav">
|
|
<h2>Översikt</h2>
|
|
<a onclick="showSection('dashboard')" class="active" id="nav-dashboard">Dashboard</a>
|
|
|
|
<h2>quiXzoom</h2>
|
|
<a onclick="showSection('missions')" id="nav-missions">Missions</a>
|
|
<a onclick="showSection('contributors')" id="nav-contributors">Contributors</a>
|
|
|
|
<h2>Ekonomi</h2>
|
|
<a onclick="showSection('ledger')" id="nav-ledger">Bokföring</a>
|
|
<a onclick="showSection('accounts')" id="nav-accounts">Konton</a>
|
|
|
|
<h2>Kommunikation</h2>
|
|
<a onclick="showSection('mail')" id="nav-mail">📧 Mail</a>
|
|
|
|
<h2>System</h2>
|
|
<a onclick="showSection('health')" id="nav-health">Hälsa</a>
|
|
</nav>
|
|
|
|
<main class="main">
|
|
|
|
<!-- DASHBOARD -->
|
|
<div id="dashboard" class="page-section">
|
|
<div class="alert">
|
|
<strong>🚨 AKUT:</strong> Momsdeklaration deadline 26 juli (20 dagar). Belopp: ~442 000 kr.
|
|
</div>
|
|
<div class="alert warning">
|
|
<strong>⚠️ VARNING:</strong> Leon-kravbrev deadline 13 juli (7 dagar). Fordran: 197 922 kr.
|
|
</div>
|
|
|
|
<h2 class="section-title">quiXzoom — LIVE</h2>
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h3>Totalt Missions</h3>
|
|
<div class="value" id="stat-missions-total">-</div>
|
|
<div class="subtitle">i databasen</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Aktiva Missions</h3>
|
|
<div class="value" id="stat-missions-active">-</div>
|
|
<div class="subtitle">redo att tas</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Submissions</h3>
|
|
<div class="value" id="stat-submissions">-</div>
|
|
<div class="subtitle">inskickade</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Contributors</h3>
|
|
<div class="value" id="stat-contributors">-</div>
|
|
<div class="subtitle">unika användare</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="section-title">Ekonomi — LIVE</h2>
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h3>Bokföringskonton</h3>
|
|
<div class="value" id="stat-accounts">-</div>
|
|
<div class="subtitle">i BAS-kontoplan</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Kassa (SEK)</h3>
|
|
<div class="value" id="stat-cash">-</div>
|
|
<div class="subtitle">företagskonto</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Trial Balance</h3>
|
|
<div class="value" id="stat-balanced">-</div>
|
|
<div class="subtitle" id="stat-balanced-sub">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="section-title">📧 Mail — LIVE</h2>
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h3>Inkorg</h3>
|
|
<div class="value" id="stat-mail-inbox">-</div>
|
|
<div class="subtitle">totalt</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Olästa</h3>
|
|
<div class="value" id="stat-mail-unread">-</div>
|
|
<div class="subtitle">kräver åtgärd</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Skickat</h3>
|
|
<div class="value" id="stat-mail-sent">-</div>
|
|
<div class="subtitle">utgående</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MISSIONS -->
|
|
<div id="missions" class="page-section hidden">
|
|
<h2 class="section-title">quiXzoom Missions — LIVE FRÅN DATABAS</h2>
|
|
<div class="card">
|
|
<div id="missions-table"><p class="loading">Laddar missions från PostgreSQL...</p></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CONTRIBUTORS -->
|
|
<div id="contributors" class="page-section hidden">
|
|
<h2 class="section-title">Contributors — LIVE FRÅN DATABAS</h2>
|
|
<div class="card">
|
|
<div id="contributors-table"><p class="loading">Laddar contributors från PostgreSQL...</p></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LEDGER -->
|
|
<div id="ledger" class="page-section hidden">
|
|
<h2 class="section-title">Bokföring — LIVE FRÅN DATABAS</h2>
|
|
<div class="card">
|
|
<h3>Trial Balance</h3>
|
|
<div id="trial-balance-table"><p class="loading">Laddar trial balance...</p></div>
|
|
</div>
|
|
<div class="card" style="margin-top: 1rem;">
|
|
<h3>Senaste verifikat</h3>
|
|
<div id="journal-table"><p class="loading">Laddar verifikat...</p></div>
|
|
</div>
|
|
<div class="card" style="margin-top: 1rem;">
|
|
<h3>Perioder</h3>
|
|
<div id="periods-table"><p class="loading">Laddar perioder...</p></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ACCOUNTS -->
|
|
<div id="accounts" class="page-section hidden">
|
|
<h2 class="section-title">Konton — LIVE FRÅN DATABAS</h2>
|
|
<div class="card">
|
|
<div id="accounts-table"><p class="loading">Laddar konton...</p></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MAIL -->
|
|
<div id="mail" class="page-section hidden">
|
|
<h2 class="section-title">📧 Mail — LIVE FRÅN mail.aamos.systems</h2>
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h3>Inkorg</h3>
|
|
<div class="value" id="mail-inbox">-</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Olästa</h3>
|
|
<div class="value" id="mail-unread">-</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Skickat</h3>
|
|
<div class="value" id="mail-sent">-</div>
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-top: 1rem;">
|
|
<h3>Meddelanden</h3>
|
|
<div id="mail-messages"><p class="loading">Laddar mail...</p></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- HEALTH -->
|
|
<div id="health" class="page-section hidden">
|
|
<h2 class="section-title">Systemhälsa</h2>
|
|
<div class="card">
|
|
<div id="health-table"><p class="loading">Laddar systemstatus...</p></div>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<script>
|
|
// API Configuration — relativa URL:er för att fungera via nginx proxy
|
|
const API_BASE = '';
|
|
|
|
function showSection(section) {
|
|
document.querySelectorAll('.page-section').forEach(s => s.classList.add('hidden'));
|
|
document.getElementById(section).classList.remove('hidden');
|
|
document.querySelectorAll('.nav a').forEach(a => a.classList.remove('active'));
|
|
document.getElementById('nav-' + section).classList.add('active');
|
|
}
|
|
|
|
async function loadDashboard() {
|
|
try {
|
|
const qxRes = await fetch(`${API_BASE}/api/v1/quixzoom/stats`);
|
|
if (qxRes.ok) {
|
|
const qxData = await qxRes.json();
|
|
document.getElementById('stat-missions-total').textContent = qxData.missions_total || '-';
|
|
document.getElementById('stat-missions-active').textContent = qxData.missions_active || '-';
|
|
document.getElementById('stat-submissions').textContent = qxData.submissions_total || '-';
|
|
document.getElementById('stat-contributors').textContent = qxData.contributors_total || '-';
|
|
}
|
|
} catch (e) {
|
|
console.error('Quixzoom error:', e);
|
|
}
|
|
|
|
try {
|
|
const accRes = await fetch(`${API_BASE}/api/ledger/accounts`);
|
|
if (accRes.ok) {
|
|
const accData = await accRes.json();
|
|
document.getElementById('stat-accounts').textContent = accData.accounts?.length || '-';
|
|
const bank = accData.accounts?.find(a => a.account_number === '1930');
|
|
if (bank) {
|
|
document.getElementById('stat-cash').textContent = new Intl.NumberFormat('sv-SE').format(bank.balance) + ' kr';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Ledger error:', e);
|
|
}
|
|
|
|
try {
|
|
const tbRes = await fetch(`${API_BASE}/api/ledger/trial-balance`);
|
|
if (tbRes.ok) {
|
|
const tbData = await tbRes.json();
|
|
document.getElementById('stat-balanced').textContent = tbData.balanced ? '✓' : '✗';
|
|
document.getElementById('stat-balanced').style.color = tbData.balanced ? 'var(--success)' : 'var(--danger)';
|
|
document.getElementById('stat-balanced-sub').textContent = tbData.balanced ? 'BALANSERAD' : 'OBALANSERAD';
|
|
}
|
|
} catch (e) {
|
|
console.error('Trial balance error:', e);
|
|
}
|
|
|
|
try {
|
|
const mailRes = await fetch(`${API_BASE}/api/mail/stats`);
|
|
if (mailRes.ok) {
|
|
const mailData = await mailRes.json();
|
|
document.getElementById('stat-mail-inbox').textContent = mailData.inbox_total || '-';
|
|
document.getElementById('stat-mail-unread').textContent = mailData.unread_count || '-';
|
|
document.getElementById('stat-mail-sent').textContent = mailData.sent_total || '-';
|
|
}
|
|
} catch (e) {
|
|
console.error('Mail error:', e);
|
|
}
|
|
}
|
|
|
|
async function loadMissions() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/v1/quixzoom/missions`);
|
|
const data = await res.json();
|
|
|
|
let html = '<table><thead><tr><th>ID</th><th>Titel</th><th>Status</th><th>Belöning</th><th>Skapad</th></tr></thead><tbody>';
|
|
data.missions.forEach(m => {
|
|
html += `<tr>
|
|
<td>${m.id.substring(0,8)}...</td>
|
|
<td>${m.title}</td>
|
|
<td><span class="badge badge-success">${m.status}</span></td>
|
|
<td>${m.reward_sek} kr</td>
|
|
<td>${new Date(m.created_at).toLocaleDateString('sv-SE')}</td>
|
|
</tr>`;
|
|
});
|
|
html += '</tbody></table>';
|
|
document.getElementById('missions-table').innerHTML = html;
|
|
} catch (e) {
|
|
document.getElementById('missions-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function loadContributors() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/v1/quixzoom/contributors`);
|
|
const data = await res.json();
|
|
|
|
let html = '<table><thead><tr><th>Namn</th><th>Email</th><th>Status</th><th>Submissions</th><th>Tjänat</th></tr></thead><tbody>';
|
|
data.contributors.forEach(c => {
|
|
html += `<tr>
|
|
<td>${c.name || '-'}</td>
|
|
<td>${c.email}</td>
|
|
<td><span class="badge badge-success">${c.status}</span></td>
|
|
<td>${c.submission_count}</td>
|
|
<td>${c.total_earnings} kr</td>
|
|
</tr>`;
|
|
});
|
|
html += '</tbody></table>';
|
|
document.getElementById('contributors-table').innerHTML = html;
|
|
} catch (e) {
|
|
document.getElementById('contributors-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function loadAccounts() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/ledger/accounts`);
|
|
const data = await res.json();
|
|
|
|
let html = '<table><thead><tr><th>Konto</th><th>Namn</th><th>Typ</th><th>Debet</th><th>Kredit</th><th>Saldo</th></tr></thead><tbody>';
|
|
data.accounts.forEach(a => {
|
|
const balance = parseFloat(a.balance);
|
|
html += `<tr>
|
|
<td>${a.account_number}</td>
|
|
<td>${a.name}</td>
|
|
<td>${a.account_type}</td>
|
|
<td>${parseFloat(a.total_debit).toFixed(2)}</td>
|
|
<td>${parseFloat(a.total_credit).toFixed(2)}</td>
|
|
<td style="color: ${balance >= 0 ? 'var(--success)' : 'var(--danger)'}">${balance.toFixed(2)}</td>
|
|
</tr>`;
|
|
});
|
|
html += '</tbody></table>';
|
|
document.getElementById('accounts-table').innerHTML = html;
|
|
} catch (e) {
|
|
document.getElementById('accounts-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function loadTrialBalance() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/ledger/trial-balance`);
|
|
const data = await res.json();
|
|
|
|
let html = '<table><thead><tr><th>Konto</th><th>Namn</th><th>Typ</th><th>Debet</th><th>Kredit</th></tr></thead><tbody>';
|
|
data.rows.forEach(r => {
|
|
html += `<tr>
|
|
<td>${r.account_number}</td>
|
|
<td>${r.name}</td>
|
|
<td>${r.account_type}</td>
|
|
<td>${parseFloat(r.total_debit).toFixed(2)}</td>
|
|
<td>${parseFloat(r.total_credit).toFixed(2)}</td>
|
|
</tr>`;
|
|
});
|
|
html += `<tr style="font-weight: bold; border-top: 2px solid var(--border)">
|
|
<td colspan="3">TOTALT</td>
|
|
<td>${data.total_debit.toFixed(2)}</td>
|
|
<td>${data.total_credit.toFixed(2)}</td>
|
|
</tr>`;
|
|
html += '</tbody></table>';
|
|
html += `<p style="margin-top: 1rem; color: ${data.balanced ? 'var(--success)' : 'var(--danger)'}">
|
|
${data.balanced ? '✓ BALANSERAD' : '✗ OBALANSERAD'}
|
|
</p>`;
|
|
document.getElementById('trial-balance-table').innerHTML = html;
|
|
} catch (e) {
|
|
document.getElementById('trial-balance-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function loadJournal() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/ledger/journal`);
|
|
const data = await res.json();
|
|
|
|
let html = '<table><thead><tr><th>#</th><th>Datum</th><th>Beskrivning</th><th>Period</th><th>Belopp</th></tr></thead><tbody>';
|
|
data.entries.forEach(e => {
|
|
const amt = e.lines ? e.lines.reduce((s, l) => s + (l.debit || 0), 0) : 0;
|
|
html += `<tr>
|
|
<td>${e.entry_number}</td>
|
|
<td>${e.entry_date}</td>
|
|
<td>${e.description}</td>
|
|
<td>${e.period}</td>
|
|
<td>${amt.toLocaleString('sv-SE')} kr</td>
|
|
</tr>`;
|
|
});
|
|
html += '</tbody></table>';
|
|
document.getElementById('journal-table').innerHTML = html;
|
|
} catch (e) {
|
|
document.getElementById('journal-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function loadPeriods() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/ledger/periods`);
|
|
const data = await res.json();
|
|
|
|
let html = '<table><thead><tr><th>Period</th><th>År</th><th>Status</th><th>Stängd</th></tr></thead><tbody>';
|
|
data.periods.forEach(p => {
|
|
html += `<tr>
|
|
<td>${p.period}</td>
|
|
<td>${p.fiscal_year}</td>
|
|
<td><span class="badge ${p.status === 'open' ? 'badge-success' : 'badge-warning'}">${p.status}</span></td>
|
|
<td>${p.closed_at || '-'}</td>
|
|
</tr>`;
|
|
});
|
|
html += '</tbody></table>';
|
|
document.getElementById('periods-table').innerHTML = html;
|
|
} catch (e) {
|
|
document.getElementById('periods-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function loadMail() {
|
|
try {
|
|
const statsRes = await fetch(`${API_BASE}/api/mail/stats`);
|
|
const statsData = await statsRes.json();
|
|
document.getElementById('mail-inbox').textContent = statsData.inbox_total || '-';
|
|
document.getElementById('mail-unread').textContent = statsData.unread_count || '-';
|
|
document.getElementById('mail-sent').textContent = statsData.sent_total || '-';
|
|
|
|
const msgRes = await fetch(`${API_BASE}/api/mail/messages?folder=inbox`);
|
|
const msgData = await msgRes.json();
|
|
|
|
let html = '<table><thead><tr><th>Från</th><th>Ämne</th><th>Status</th><th>Datum</th></tr></thead><tbody>';
|
|
msgData.messages.forEach(m => {
|
|
html += `<tr class="${m.read ? '' : 'mail-unread'}">
|
|
<td>${m.sender}</td>
|
|
<td>${m.subject}</td>
|
|
<td><span class="badge ${m.read ? 'badge-success' : 'badge-blue'}">${m.read ? 'Läst' : 'Oläst'}</span></td>
|
|
<td>${new Date(m.created_at).toLocaleDateString('sv-SE')}</td>
|
|
</tr>`;
|
|
});
|
|
html += '</tbody></table>';
|
|
document.getElementById('mail-messages').innerHTML = html;
|
|
} catch (e) {
|
|
document.getElementById('mail-messages').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function loadHealth() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/v1/system/services`);
|
|
const data = await res.json();
|
|
|
|
let html = '<table><thead><tr><th>Tjänst</th><th>Port</th><th>Status</th></tr></thead><tbody>';
|
|
data.services.forEach(s => {
|
|
html += `<tr>
|
|
<td>${s.name}</td>
|
|
<td>${s.port}</td>
|
|
<td><span class="badge ${s.status === 'running' ? 'badge-success' : 'badge-danger'}">${s.status}</span></td>
|
|
</tr>`;
|
|
});
|
|
html += '</tbody></table>';
|
|
document.getElementById('health-table').innerHTML = html;
|
|
} catch (e) {
|
|
document.getElementById('health-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
// Load all data
|
|
loadDashboard();
|
|
loadMissions();
|
|
loadContributors();
|
|
loadAccounts();
|
|
loadTrialBalance();
|
|
loadJournal();
|
|
loadPeriods();
|
|
loadMail();
|
|
loadHealth();
|
|
|
|
// Refresh every 30 seconds
|
|
setInterval(() => {
|
|
loadDashboard();
|
|
loadMissions();
|
|
loadMail();
|
|
}, 30000);
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|