Files
boc/aamos-admin-upgrade/dist/unified-dashboard.html
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

425 lines
13 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" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AAMOS Admin — Unified Dashboard</title>
<link rel="stylesheet" href="assets/admin.css">
<style>
/* Alert banner */
.alert-banner {
background: linear-gradient(135deg, #FF3B30, #FF6B6B);
color: white;
padding: 12px 20px;
border-radius: 12px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
animation: slideDown 0.4s ease;
}
.alert-banner.warning {
background: linear-gradient(135deg, #FF9500, #FFB84D);
}
.alert-banner.info {
background: linear-gradient(135deg, #007AFF, #5AC8FA);
}
.alert-banner .alert-icon { font-size: 22px; }
.alert-banner .alert-content { flex: 1; }
.alert-banner .alert-title { font-weight: 700; font-size: 15px; }
.alert-banner .alert-meta { font-size: 13px; opacity: 0.9; }
.alert-banner .alert-amount { font-weight: 700; font-size: 18px; }
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: none; }
}
/* System cards grid */
.systems-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.system-card {
background: var(--bg-card);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow);
transition: transform 0.2s, box-shadow 0.2s;
border-left: 4px solid var(--primary);
}
.system-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.system-card.quixzoom { border-left-color: #34C759; }
.system-card.landvex { border-left-color: #AF52DE; }
.system-card.economy { border-left-color: #FF9500; }
.system-card.crm { border-left-color: #32ADE6; }
.system-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.system-icon {
width: 40px; height: 40px;
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 20px;
}
.system-card.quixzoom .system-icon { background: rgba(52,199,89,0.12); }
.system-card.landvex .system-icon { background: rgba(175,82,222,0.12); }
.system-card.economy .system-icon { background: rgba(255,149,0,0.12); }
.system-card.crm .system-icon { background: rgba(50,173,230,0.12); }
.system-name { font-size: 17px; font-weight: 700; }
.system-status {
margin-left: auto;
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 20px;
background: rgba(52,199,89,0.12);
color: var(--success);
}
.metrics-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.metric-item {
text-align: center;
padding: 10px;
background: var(--bg-input);
border-radius: var(--radius-sm);
}
.metric-value {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
}
.metric-label {
font-size: 12px;
color: var(--text-tertiary);
margin-top: 2px;
}
/* Service health mini */
.health-mini {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
.health-pill {
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
background: var(--bg-input);
}
.health-pill.ok { background: rgba(52,199,89,0.12); color: var(--success); }
.health-pill.warn { background: rgba(255,149,0,0.12); color: var(--warning); }
.health-pill.error { background: rgba(255,59,48,0.12); color: var(--danger); }
/* Cash position */
.cash-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 12px;
}
.cash-item {
text-align: center;
padding: 12px;
background: var(--bg-input);
border-radius: var(--radius-sm);
}
.cash-value { font-size: 20px; font-weight: 700; }
.cash-label { font-size: 11px; color: var(--text-tertiary); }
/* Deadline countdown */
.countdown {
font-family: 'SF Mono', monospace;
font-size: 14px;
font-weight: 600;
color: #FF3B30;
}
</style>
</head>
<body>
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-brand">
<div class="brand-icon">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.18L20 8.5v7L12 19.82 4 15.5v-7l8-4.32z"/></svg>
</div>
<div class="brand-text">
<div class="brand-name">AAMOS Admin</div>
<div class="brand-sub">Unified v2.0</div>
</div>
</div>
<nav class="sidebar-nav">
<div class="nav-section-label">Overview</div>
<a class="nav-item active" href="unified-dashboard.html">
<span class="nav-icon">📊</span>
Dashboard
</a>
<a class="nav-item" href="users.html">
<span class="nav-icon">👥</span>
Users
</a>
<a class="nav-item" href="modules.html">
<span class="nav-icon">🔧</span>
Modules
</a>
<div class="nav-section-label">Systems</div>
<a class="nav-item" href="quixzoom.html">
<span class="nav-icon">📸</span>
quiXzoom
</a>
<a class="nav-item" href="landvex.html">
<span class="nav-icon">🌍</span>
LandveX
</a>
<a class="nav-item" href="economy.html">
<span class="nav-icon">💰</span>
Economy
</a>
<a class="nav-item" href="crm.html">
<span class="nav-icon">🤝</span>
CRM
</a>
<div class="nav-section-label">System</div>
<a class="nav-item" href="audit.html">
<span class="nav-icon">📋</span>
Audit Log
</a>
<a class="nav-item" href="settings.html">
<span class="nav-icon">⚙️</span>
Settings
</a>
</nav>
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="user-avatar" id="user-avatar">SA</div>
<div class="user-info">
<div class="user-name" id="user-name">System Admin</div>
<div class="user-role">Super Admin</div>
</div>
</div>
</div>
</aside>
<!-- Main -->
<div class="main">
<header class="topbar">
<div class="topbar-title">Unified Dashboard</div>
<div class="topbar-actions">
<div class="refresh-indicator">
<div class="refresh-dot" id="refresh-dot"></div>
<span id="refresh-label">Live</span>
</div>
<button class="icon-btn" id="refresh-btn" title="Refresh">🔄</button>
</div>
</header>
<section class="page-content active">
<!-- Alerts -->
<div id="alerts-container"></div>
<!-- Systems Overview -->
<div class="systems-grid" id="systems-grid">
<!-- Filled by JS -->
</div>
<!-- Service Health -->
<div class="card">
<div class="section-header">
<span class="section-title">Service Health</span>
<span id="services-status">Loading...</span>
</div>
<div id="services-list" class="health-mini"></div>
</div>
</section>
</div>
<script src="assets/api.js"></script>
<script>
'use strict';
const api = createAdminAPI(window.location.origin);
function formatNumber(n) {
return n?.toLocaleString('sv-SE') || '—';
}
function formatCurrency(sek) {
if (!sek) return '—';
return new Intl.NumberFormat('sv-SE', {
style: 'currency', currency: 'SEK', maximumFractionDigits: 0
}).format(sek);
}
function daysUntil(date) {
const diff = Math.ceil((new Date(date) - Date.now()) / (1000 * 60 * 60 * 24));
return diff;
}
function renderAlerts(alerts) {
const container = document.getElementById('alerts-container');
if (!alerts?.length) {
container.innerHTML = '';
return;
}
container.innerHTML = alerts.map(a => {
const days = a.deadline ? daysUntil(a.deadline) : null;
const levelClass = a.level === 'critical' ? '' : a.level;
const icon = a.level === 'critical' ? '🚨' : a.level === 'warning' ? '⚠️' : '️';
const countdown = days !== null ? `<span class="countdown">${days} dagar kvar</span>` : '';
const amount = a.amount_sek ? `<span class="alert-amount">${formatCurrency(a.amount_sek)}</span>` : '';
return `
<div class="alert-banner ${levelClass}">
<span class="alert-icon">${icon}</span>
<div class="alert-content">
<div class="alert-title">${a.message}</div>
<div class="alert-meta">${countdown} ${amount}</div>
</div>
</div>
`;
}).join('');
}
function renderSystems(data) {
const grid = document.getElementById('systems-grid');
const systems = [
{
id: 'quixzoom',
name: 'quiXzoom',
icon: '📸',
metrics: [
{ label: 'Missions', value: data.quixzoom?.total_missions },
{ label: 'Active', value: data.quixzoom?.active_missions },
{ label: 'Submissions', value: data.quixzoom?.total_submissions },
{ label: 'Contributors', value: data.quixzoom?.contributors },
]
},
{
id: 'landvex',
name: 'LandveX',
icon: '🌍',
metrics: [
{ label: 'Reports', value: data.landvex?.intelligence_reports },
{ label: 'Data Points', value: data.landvex?.urban_data_points },
{ label: 'Customers', value: data.landvex?.active_customers },
{ label: 'Modules', value: data.modules_total },
]
},
{
id: 'economy',
name: 'Economy',
icon: '💰',
metrics: [
{ label: 'Vouchers', value: data.economy?.total_vouchers },
{ label: 'Accounts', value: data.economy?.total_accounts },
{ label: 'Pending', value: data.economy?.pending_invoices },
{ label: 'Balance', value: data.economy?.trial_balance },
]
},
{
id: 'crm',
name: 'CRM',
icon: '🤝',
metrics: [
{ label: 'Customers', value: data.crm?.total_customers },
{ label: 'Leads', value: data.crm?.active_leads },
{ label: 'Won', value: data.crm?.deals_won },
{ label: 'Pipeline', value: data.crm?.pipeline_value_sek },
]
}
];
grid.innerHTML = systems.map(sys => `
<div class="system-card ${sys.id}">
<div class="system-header">
<span class="system-icon">${sys.icon}</span>
<span class="system-name">${sys.name}</span>
<span class="system-status">● Live</span>
</div>
<div class="metrics-row">
${sys.metrics.map(m => `
<div class="metric-item">
<div class="metric-value">${typeof m.value === 'number' && m.value > 1000 ? formatNumber(m.value) : m.value || '—'}</div>
<div class="metric-label">${m.label}</div>
</div>
`).join('')}
</div>
</div>
`).join('');
}
function renderServices(services) {
const list = document.getElementById('services-list');
const status = document.getElementById('services-status');
if (!services?.length) {
list.innerHTML = '<span style="color:var(--text-tertiary)">No services found</span>';
status.textContent = '';
return;
}
const healthy = services.filter(s => s.status === 'running').length;
status.textContent = `${healthy}/${services.length} healthy`;
list.innerHTML = services.slice(0, 12).map(s => {
const cls = s.status === 'running' ? 'ok' : s.status === 'degraded' ? 'warn' : 'error';
return `<span class="health-pill ${cls}">${s.name}</span>`;
}).join('');
}
async function loadDashboard() {
const dot = document.getElementById('refresh-dot');
dot.classList.add('loading');
try {
// Load unified dashboard
const data = await api._get('/api/v1/unified-dashboard');
renderAlerts(data.alerts);
renderSystems(data);
// Load service health
const health = await api._get('/api/v1/service-health');
renderServices(health.services);
document.getElementById('refresh-label').textContent = 'Live';
} catch (err) {
console.error('Dashboard load error:', err);
document.getElementById('refresh-label').textContent = 'Error';
dot.style.background = 'var(--danger)';
} finally {
dot.classList.remove('loading');
}
}
// Init
loadDashboard();
setInterval(loadDashboard, 30000);
document.getElementById('refresh-btn').addEventListener('click', loadDashboard);
</script>
</body>
</html>