6de2455917
- 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
1271 lines
54 KiB
HTML
1271 lines
54 KiB
HTML
<!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</title>
|
|
<style>
|
|
:root {
|
|
--bg-primary: #F2F2F7;
|
|
--bg-card: #FFFFFF;
|
|
--bg-sidebar: #FFFFFF;
|
|
--bg-input: #F2F2F7;
|
|
--text-primary: #000000;
|
|
--text-secondary: #3C3C43;
|
|
--text-tertiary: #8E8E93;
|
|
--separator: rgba(60,60,67,0.12);
|
|
--shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
--shadow-md: 0 4px 24px rgba(0,0,0,0.12);
|
|
--primary: #007AFF;
|
|
--success: #34C759;
|
|
--warning: #FF9500;
|
|
--danger: #FF3B30;
|
|
--purple: #AF52DE;
|
|
--teal: #32ADE6;
|
|
--radius: 12px;
|
|
--radius-sm: 8px;
|
|
--sidebar-w: 240px;
|
|
--tabbar-h: 83px;
|
|
--header-h: 60px;
|
|
--transition: 0.3s ease;
|
|
}
|
|
[data-theme="dark"] {
|
|
--bg-primary: #000000;
|
|
--bg-card: #1C1C1E;
|
|
--bg-sidebar: #1C1C1E;
|
|
--bg-input: #2C2C2E;
|
|
--text-primary: #FFFFFF;
|
|
--text-secondary: rgba(235,235,245,0.6);
|
|
--text-tertiary: rgba(235,235,245,0.3);
|
|
--separator: rgba(84,84,88,0.65);
|
|
--shadow: 0 2px 12px rgba(0,0,0,0.4);
|
|
--shadow-md: 0 4px 24px rgba(0,0,0,0.6);
|
|
}
|
|
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
html { font-size: 16px; }
|
|
|
|
body {
|
|
font-family: -apple-system, "SF Pro Display", "SF Pro Text", system-ui, sans-serif;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
transition: background var(--transition), color var(--transition);
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
/* ── Sidebar ─────────────────────────────────────── */
|
|
.sidebar {
|
|
width: var(--sidebar-w);
|
|
background: var(--bg-sidebar);
|
|
border-right: 1px solid var(--separator);
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: fixed;
|
|
top: 0; left: 0; bottom: 0;
|
|
z-index: 100;
|
|
transition: background var(--transition), border-color var(--transition);
|
|
}
|
|
|
|
.sidebar-brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 20px 20px 16px;
|
|
border-bottom: 1px solid var(--separator);
|
|
}
|
|
.sidebar-brand .brand-icon {
|
|
width: 36px; height: 36px;
|
|
background: var(--primary);
|
|
border-radius: 10px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.sidebar-brand .brand-icon svg { width: 20px; height: 20px; fill: #fff; }
|
|
.sidebar-brand .brand-text { line-height: 1.1; }
|
|
.sidebar-brand .brand-name {
|
|
font-size: 15px; font-weight: 700;
|
|
letter-spacing: -0.3px;
|
|
color: var(--text-primary);
|
|
}
|
|
.sidebar-brand .brand-sub {
|
|
font-size: 11px;
|
|
color: var(--text-tertiary);
|
|
letter-spacing: 0.2px;
|
|
}
|
|
|
|
.sidebar-nav { flex: 1; padding: 12px 0; overflow-y: auto; }
|
|
|
|
.nav-section-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.5px;
|
|
text-transform: uppercase;
|
|
color: var(--text-tertiary);
|
|
padding: 12px 20px 4px;
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 16px;
|
|
margin: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
transition: background var(--transition), color var(--transition);
|
|
color: var(--text-secondary);
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
user-select: none;
|
|
}
|
|
.nav-item:hover { background: var(--bg-input); color: var(--text-primary); }
|
|
.nav-item.active {
|
|
background: rgba(0,122,255,0.12);
|
|
color: var(--primary);
|
|
}
|
|
.nav-item .nav-icon {
|
|
width: 32px; height: 32px;
|
|
border-radius: 8px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0;
|
|
font-size: 16px;
|
|
background: var(--bg-input);
|
|
transition: background var(--transition);
|
|
}
|
|
.nav-item.active .nav-icon {
|
|
background: var(--primary);
|
|
}
|
|
.nav-item.active .nav-icon svg { fill: #fff; }
|
|
.nav-item .nav-icon svg { width: 16px; height: 16px; fill: var(--text-secondary); transition: fill var(--transition); }
|
|
.nav-item.active .nav-icon svg { fill: #fff; }
|
|
.nav-item:hover:not(.active) .nav-icon svg { fill: var(--text-primary); }
|
|
|
|
.nav-badge {
|
|
margin-left: auto;
|
|
background: var(--danger);
|
|
color: #fff;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
padding: 1px 6px;
|
|
border-radius: 10px;
|
|
min-width: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.sidebar-footer {
|
|
padding: 12px 8px;
|
|
border-top: 1px solid var(--separator);
|
|
}
|
|
.sidebar-user {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
transition: background var(--transition);
|
|
}
|
|
.sidebar-user:hover { background: var(--bg-input); }
|
|
.user-avatar {
|
|
width: 34px; height: 34px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--primary), var(--purple));
|
|
display: flex; align-items: center; justify-content: center;
|
|
color: #fff; font-weight: 700; font-size: 13px;
|
|
flex-shrink: 0;
|
|
}
|
|
.user-info { flex: 1; min-width: 0; }
|
|
.user-name { font-size: 13px; font-weight: 600; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.user-role { font-size: 11px; color: var(--text-tertiary); }
|
|
|
|
/* ── Main area ───────────────────────────────────── */
|
|
.main {
|
|
margin-left: var(--sidebar-w);
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* ── Topbar ──────────────────────────────────────── */
|
|
.topbar {
|
|
height: var(--header-h);
|
|
background: var(--bg-sidebar);
|
|
border-bottom: 1px solid var(--separator);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 24px;
|
|
gap: 16px;
|
|
position: sticky;
|
|
top: 0; z-index: 50;
|
|
transition: background var(--transition);
|
|
}
|
|
.topbar-title {
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.4px;
|
|
flex: 1;
|
|
}
|
|
.topbar-actions { display: flex; align-items: center; gap: 8px; }
|
|
|
|
.icon-btn {
|
|
width: 36px; height: 36px;
|
|
border: none;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-input);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center;
|
|
transition: background var(--transition), color var(--transition);
|
|
}
|
|
.icon-btn:hover { background: var(--separator); color: var(--text-primary); }
|
|
.icon-btn svg { width: 18px; height: 18px; fill: currentColor; }
|
|
|
|
.refresh-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
color: var(--text-tertiary);
|
|
}
|
|
.refresh-dot {
|
|
width: 7px; height: 7px;
|
|
border-radius: 50%;
|
|
background: var(--success);
|
|
}
|
|
.refresh-dot.loading {
|
|
animation: pulse 1s infinite;
|
|
}
|
|
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
|
|
|
|
/* ── Page content ────────────────────────────────── */
|
|
.page-content {
|
|
padding: 24px;
|
|
display: none;
|
|
}
|
|
.page-content.active { display: block; }
|
|
|
|
/* ── Section header ──────────────────────────────── */
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
}
|
|
.section-title {
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.3px;
|
|
}
|
|
.section-action {
|
|
font-size: 15px;
|
|
color: var(--primary);
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
}
|
|
|
|
/* ── KPI Grid ────────────────────────────────────── */
|
|
.kpi-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.kpi-card {
|
|
background: var(--bg-card);
|
|
border-radius: var(--radius);
|
|
padding: 20px;
|
|
box-shadow: var(--shadow);
|
|
transition: box-shadow var(--transition), background var(--transition), transform var(--transition);
|
|
cursor: default;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.kpi-card:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
|
|
|
|
.kpi-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0; left: 0; right: 0;
|
|
height: 3px;
|
|
border-radius: var(--radius) var(--radius) 0 0;
|
|
}
|
|
.kpi-card.primary::before { background: var(--primary); }
|
|
.kpi-card.success::before { background: var(--success); }
|
|
.kpi-card.warning::before { background: var(--warning); }
|
|
.kpi-card.purple::before { background: var(--purple); }
|
|
|
|
.kpi-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 12px;
|
|
}
|
|
.kpi-label {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
}
|
|
.kpi-icon {
|
|
width: 36px; height: 36px;
|
|
border-radius: 10px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.kpi-icon svg { width: 18px; height: 18px; }
|
|
.kpi-card.primary .kpi-icon { background: rgba(0,122,255,0.12); }
|
|
.kpi-card.primary .kpi-icon svg { fill: var(--primary); }
|
|
.kpi-card.success .kpi-icon { background: rgba(52,199,89,0.12); }
|
|
.kpi-card.success .kpi-icon svg { fill: var(--success); }
|
|
.kpi-card.warning .kpi-icon { background: rgba(255,149,0,0.12); }
|
|
.kpi-card.warning .kpi-icon svg { fill: var(--warning); }
|
|
.kpi-card.purple .kpi-icon { background: rgba(175,82,222,0.12); }
|
|
.kpi-card.purple .kpi-icon svg { fill: var(--purple); }
|
|
|
|
.kpi-value {
|
|
font-size: 34px;
|
|
font-weight: 700;
|
|
letter-spacing: -1px;
|
|
line-height: 1;
|
|
margin-bottom: 4px;
|
|
}
|
|
.kpi-sub {
|
|
font-size: 13px;
|
|
color: var(--text-tertiary);
|
|
}
|
|
.kpi-sub .up { color: var(--success); font-weight: 600; }
|
|
.kpi-sub .down { color: var(--danger); font-weight: 600; }
|
|
.kpi-sub .warn { color: var(--warning); font-weight: 600; }
|
|
|
|
/* ── Dashboard Grid ──────────────────────────────── */
|
|
.dashboard-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 320px;
|
|
gap: 16px;
|
|
align-items: start;
|
|
}
|
|
.dashboard-left { display: flex; flex-direction: column; gap: 16px; }
|
|
.dashboard-right { display: flex; flex-direction: column; gap: 16px; }
|
|
|
|
/* ── Card ────────────────────────────────────────── */
|
|
.card {
|
|
background: var(--bg-card);
|
|
border-radius: var(--radius);
|
|
padding: 20px;
|
|
box-shadow: var(--shadow);
|
|
transition: background var(--transition), box-shadow var(--transition);
|
|
}
|
|
|
|
/* ── Health Ring ─────────────────────────────────── */
|
|
.health-ring-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 24px;
|
|
}
|
|
.ring-container {
|
|
position: relative;
|
|
width: 120px; height: 120px;
|
|
flex-shrink: 0;
|
|
}
|
|
.ring-svg { transform: rotate(-90deg); }
|
|
.ring-bg { fill: none; stroke: var(--separator); stroke-width: 10; }
|
|
.ring-fill {
|
|
fill: none;
|
|
stroke-width: 10;
|
|
stroke-linecap: round;
|
|
transition: stroke-dashoffset 1.2s cubic-bezier(0.4,0,0.2,1), stroke 0.3s ease;
|
|
}
|
|
.ring-label {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.ring-pct {
|
|
font-size: 26px;
|
|
font-weight: 800;
|
|
letter-spacing: -1px;
|
|
line-height: 1;
|
|
}
|
|
.ring-sub-label {
|
|
font-size: 10px;
|
|
color: var(--text-tertiary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.3px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.health-stats { flex: 1; }
|
|
.health-stat-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid var(--separator);
|
|
}
|
|
.health-stat-row:last-child { border-bottom: none; }
|
|
.health-dot {
|
|
width: 10px; height: 10px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
.health-stat-name { font-size: 14px; font-weight: 500; flex: 1; }
|
|
.health-stat-val { font-size: 13px; font-weight: 600; color: var(--text-secondary); }
|
|
.health-stat-badge {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
}
|
|
.badge-success { background: rgba(52,199,89,0.15); color: var(--success); }
|
|
.badge-warning { background: rgba(255,149,0,0.15); color: var(--warning); }
|
|
.badge-danger { background: rgba(255,59,48,0.15); color: var(--danger); }
|
|
.badge-neutral { background: var(--bg-input); color: var(--text-tertiary); }
|
|
|
|
/* ── Audit Events ────────────────────────────────── */
|
|
.audit-list { display: flex; flex-direction: column; }
|
|
.audit-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 12px 0;
|
|
border-bottom: 1px solid var(--separator);
|
|
animation: fadeIn 0.4s ease;
|
|
}
|
|
.audit-row:last-child { border-bottom: none; padding-bottom: 0; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
|
|
.audit-icon {
|
|
width: 32px; height: 32px;
|
|
border-radius: 8px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0;
|
|
margin-top: 1px;
|
|
}
|
|
.audit-icon svg { width: 15px; height: 15px; }
|
|
.audit-icon.info { background: rgba(0,122,255,0.12); }
|
|
.audit-icon.info svg { fill: var(--primary); }
|
|
.audit-icon.success { background: rgba(52,199,89,0.12); }
|
|
.audit-icon.success svg { fill: var(--success); }
|
|
.audit-icon.warning { background: rgba(255,149,0,0.12); }
|
|
.audit-icon.warning svg { fill: var(--warning); }
|
|
.audit-icon.danger { background: rgba(255,59,48,0.12); }
|
|
.audit-icon.danger svg { fill: var(--danger); }
|
|
|
|
.audit-body { flex: 1; min-width: 0; }
|
|
.audit-event { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.audit-meta { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
|
|
.audit-time { font-size: 12px; color: var(--text-tertiary); white-space: nowrap; flex-shrink: 0; }
|
|
|
|
/* ── Quick Actions ───────────────────────────────── */
|
|
.qa-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
}
|
|
.qa-btn {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 6px;
|
|
padding: 14px;
|
|
border: none;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
transition: background var(--transition), transform var(--transition), box-shadow var(--transition);
|
|
text-align: left;
|
|
}
|
|
.qa-btn:hover { transform: translateY(-1px); box-shadow: var(--shadow); }
|
|
.qa-btn:active { transform: translateY(0); }
|
|
.qa-btn .qa-icon { font-size: 22px; }
|
|
.qa-btn .qa-label { font-size: 13px; font-weight: 600; }
|
|
.qa-btn .qa-desc { font-size: 11px; opacity: 0.75; }
|
|
|
|
.qa-btn.blue { background: rgba(0,122,255,0.1); color: var(--primary); }
|
|
.qa-btn.green { background: rgba(52,199,89,0.1); color: var(--success); }
|
|
.qa-btn.orange { background: rgba(255,149,0,0.1); color: var(--warning); }
|
|
.qa-btn.purple { background: rgba(175,82,222,0.1); color: var(--purple); }
|
|
|
|
/* ── Service Table ───────────────────────────────── */
|
|
.service-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
.service-table th {
|
|
text-align: left;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.4px;
|
|
color: var(--text-tertiary);
|
|
padding: 0 0 10px;
|
|
border-bottom: 1px solid var(--separator);
|
|
}
|
|
.service-table td {
|
|
padding: 11px 0;
|
|
border-bottom: 1px solid var(--separator);
|
|
vertical-align: middle;
|
|
}
|
|
.service-table tr:last-child td { border-bottom: none; }
|
|
.service-table td:last-child { text-align: right; }
|
|
|
|
.status-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
padding: 3px 10px;
|
|
border-radius: 20px;
|
|
}
|
|
.status-pill::before {
|
|
content: '';
|
|
width: 6px; height: 6px;
|
|
border-radius: 50%;
|
|
background: currentColor;
|
|
}
|
|
.status-pill.ok { background: rgba(52,199,89,0.12); color: var(--success); }
|
|
.status-pill.warn { background: rgba(255,149,0,0.12); color: var(--warning); }
|
|
.status-pill.error { background: rgba(255,59,48,0.12); color: var(--danger); }
|
|
|
|
.svc-name { font-weight: 500; }
|
|
.svc-latency { color: var(--text-secondary); font-size: 13px; }
|
|
|
|
/* ── Skeleton loading ────────────────────────────── */
|
|
.skeleton {
|
|
background: linear-gradient(90deg, var(--bg-input) 25%, var(--separator) 50%, var(--bg-input) 75%);
|
|
background-size: 200% 100%;
|
|
animation: shimmer 1.5s infinite;
|
|
border-radius: 6px;
|
|
}
|
|
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
|
|
|
/* ── Bottom Tab Bar (mobile) ─────────────────────── */
|
|
.tab-bar {
|
|
display: none;
|
|
position: fixed;
|
|
bottom: 0; left: 0; right: 0;
|
|
height: var(--tabbar-h);
|
|
background: var(--bg-sidebar);
|
|
border-top: 1px solid var(--separator);
|
|
padding: 8px 0 calc(8px + env(safe-area-inset-bottom));
|
|
z-index: 100;
|
|
transition: background var(--transition);
|
|
}
|
|
.tab-bar-inner {
|
|
display: flex;
|
|
justify-content: space-around;
|
|
align-items: flex-start;
|
|
}
|
|
.tab-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 3px;
|
|
cursor: pointer;
|
|
padding: 0 8px;
|
|
min-width: 60px;
|
|
}
|
|
.tab-icon {
|
|
width: 28px; height: 28px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.tab-icon svg { width: 22px; height: 22px; fill: var(--text-tertiary); transition: fill var(--transition); }
|
|
.tab-item.active .tab-icon svg { fill: var(--primary); }
|
|
.tab-label {
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
color: var(--text-tertiary);
|
|
transition: color var(--transition);
|
|
}
|
|
.tab-item.active .tab-label { color: var(--primary); }
|
|
|
|
/* ── Empty / placeholder pages ───────────────────── */
|
|
.placeholder-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 320px;
|
|
gap: 12px;
|
|
color: var(--text-tertiary);
|
|
}
|
|
.placeholder-page svg { width: 56px; height: 56px; fill: var(--separator); }
|
|
.placeholder-page h3 { font-size: 18px; font-weight: 600; color: var(--text-secondary); }
|
|
.placeholder-page p { font-size: 14px; max-width: 280px; text-align: center; }
|
|
|
|
/* ── Responsive ──────────────────────────────────── */
|
|
@media (max-width: 1100px) {
|
|
.kpi-grid { grid-template-columns: repeat(2, 1fr); }
|
|
.dashboard-grid { grid-template-columns: 1fr; }
|
|
.dashboard-right { order: -1; }
|
|
.health-ring-card { flex-wrap: wrap; }
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.sidebar { display: none; }
|
|
.tab-bar { display: block; }
|
|
.main {
|
|
margin-left: 0;
|
|
padding-bottom: var(--tabbar-h);
|
|
}
|
|
.kpi-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
|
.page-content { padding: 16px; }
|
|
.topbar { padding: 0 16px; }
|
|
.topbar-title { font-size: 17px; }
|
|
}
|
|
|
|
@media (max-width: 430px) {
|
|
.kpi-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
.kpi-value { font-size: 26px; }
|
|
.qa-grid { grid-template-columns: 1fr 1fr; }
|
|
}
|
|
|
|
/* ── Toast notification ───────────────────────────── */
|
|
.toast-container {
|
|
position: fixed;
|
|
top: 72px; right: 16px;
|
|
z-index: 200;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.toast {
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
box-shadow: var(--shadow-md);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
min-width: 240px;
|
|
max-width: 320px;
|
|
animation: slideIn 0.3s ease;
|
|
border-left: 4px solid var(--primary);
|
|
}
|
|
.toast.success { border-color: var(--success); }
|
|
.toast.error { border-color: var(--danger); }
|
|
@keyframes slideIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: none; } }
|
|
@keyframes slideOut { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateX(20px); } }
|
|
.toast.removing { animation: slideOut 0.3s ease forwards; }
|
|
</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">v2.4.0 · Production</div>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="sidebar-nav">
|
|
<div class="nav-section-label">Main</div>
|
|
<a class="nav-item active" data-page="dashboard">
|
|
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg></span>
|
|
Dashboard
|
|
</a>
|
|
<a class="nav-item" data-page="users">
|
|
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg></span>
|
|
Users
|
|
<span class="nav-badge" id="users-badge">3</span>
|
|
</a>
|
|
<a class="nav-item" data-page="modules">
|
|
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-5 4h2v8h-2V8zm-4 0h2v8h-2V8zM7 8h2v8H7V8z"/></svg></span>
|
|
Modules
|
|
</a>
|
|
<div class="nav-section-label">System</div>
|
|
<a class="nav-item" data-page="settings">
|
|
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg></span>
|
|
Settings
|
|
</a>
|
|
<a class="nav-item" data-page="audit">
|
|
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg></span>
|
|
Audit Log
|
|
</a>
|
|
<a class="nav-item" data-page="onboarding">
|
|
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg></span>
|
|
Onboarding
|
|
</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>
|
|
<button class="icon-btn" id="theme-toggle" title="Toggle theme" style="margin-left:auto;width:28px;height:28px;">
|
|
<svg id="theme-icon-moon" viewBox="0 0 24 24"><path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"/></svg>
|
|
<svg id="theme-icon-sun" viewBox="0 0 24 24" style="display:none"><path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 11H1v2h3v-2zm9-9h-2v2.99h2V2zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 11v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ──────────────── MAIN ──────────────────────────── -->
|
|
<div class="main">
|
|
|
|
<!-- Topbar -->
|
|
<header class="topbar">
|
|
<div class="topbar-title" id="page-title">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 now">
|
|
<svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
</button>
|
|
<button class="icon-btn" id="mobile-theme-btn" title="Toggle theme" style="display:none">
|
|
<svg viewBox="0 0 24 24"><path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"/></svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- ── PAGE: Dashboard ── -->
|
|
<section class="page-content active" id="page-dashboard">
|
|
|
|
<!-- KPI Cards -->
|
|
<div class="kpi-grid" id="kpi-grid">
|
|
<div class="kpi-card primary">
|
|
<div class="kpi-header">
|
|
<span class="kpi-label">Total Users</span>
|
|
<span class="kpi-icon"><svg viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg></span>
|
|
</div>
|
|
<div class="kpi-value" id="kpi-users">—</div>
|
|
<div class="kpi-sub" id="kpi-users-sub"><span class="skeleton" style="display:inline-block;width:80px;height:14px;"></span></div>
|
|
</div>
|
|
<div class="kpi-card success">
|
|
<div class="kpi-header">
|
|
<span class="kpi-label">Healthy Services</span>
|
|
<span class="kpi-icon"><svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14l-5-5 1.41-1.41L12 14.17l7.59-7.59L21 8l-9 9z"/></svg></span>
|
|
</div>
|
|
<div class="kpi-value" id="kpi-services">—</div>
|
|
<div class="kpi-sub" id="kpi-services-sub"><span class="skeleton" style="display:inline-block;width:80px;height:14px;"></span></div>
|
|
</div>
|
|
<div class="kpi-card warning">
|
|
<div class="kpi-header">
|
|
<span class="kpi-label">Active Modules</span>
|
|
<span class="kpi-icon"><svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-5 4h2v8h-2V8zm-4 0h2v8h-2V8zM7 8h2v8H7V8z"/></svg></span>
|
|
</div>
|
|
<div class="kpi-value" id="kpi-modules">—</div>
|
|
<div class="kpi-sub" id="kpi-modules-sub"><span class="skeleton" style="display:inline-block;width:80px;height:14px;"></span></div>
|
|
</div>
|
|
<div class="kpi-card purple">
|
|
<div class="kpi-header">
|
|
<span class="kpi-label">System Load</span>
|
|
<span class="kpi-icon"><svg viewBox="0 0 24 24"><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/></svg></span>
|
|
</div>
|
|
<div class="kpi-value" id="kpi-load">—</div>
|
|
<div class="kpi-sub" id="kpi-load-sub"><span class="skeleton" style="display:inline-block;width:80px;height:14px;"></span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-grid">
|
|
<div class="dashboard-left">
|
|
|
|
<!-- Service Health Table -->
|
|
<div class="card">
|
|
<div class="section-header">
|
|
<span class="section-title">Service Health</span>
|
|
<button class="section-action" data-page-link="modules">View all →</button>
|
|
</div>
|
|
<table class="service-table" id="service-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Service</th>
|
|
<th>Status</th>
|
|
<th>Latency</th>
|
|
<th>Uptime</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="service-tbody">
|
|
<tr><td colspan="4"><div class="skeleton" style="height:36px;border-radius:6px;margin:4px 0;"></div></td></tr>
|
|
<tr><td colspan="4"><div class="skeleton" style="height:36px;border-radius:6px;margin:4px 0;"></div></td></tr>
|
|
<tr><td colspan="4"><div class="skeleton" style="height:36px;border-radius:6px;margin:4px 0;"></div></td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Recent Audit Events -->
|
|
<div class="card">
|
|
<div class="section-header">
|
|
<span class="section-title">Recent Audit Events</span>
|
|
<button class="section-action" data-page-link="audit">View all →</button>
|
|
</div>
|
|
<div class="audit-list" id="audit-list">
|
|
<div class="audit-row"><div class="skeleton" style="height:44px;width:100%;border-radius:6px;"></div></div>
|
|
<div class="audit-row"><div class="skeleton" style="height:44px;width:100%;border-radius:6px;"></div></div>
|
|
<div class="audit-row"><div class="skeleton" style="height:44px;width:100%;border-radius:6px;"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="dashboard-right">
|
|
|
|
<!-- Health Ring -->
|
|
<div class="card health-ring-card">
|
|
<div class="ring-container" id="ring-container">
|
|
<svg class="ring-svg" width="120" height="120" viewBox="0 0 120 120">
|
|
<circle class="ring-bg" cx="60" cy="60" r="50"/>
|
|
<circle class="ring-fill" id="ring-path" cx="60" cy="60" r="50"
|
|
stroke-dasharray="314"
|
|
stroke-dashoffset="314"
|
|
stroke="var(--success)"/>
|
|
</svg>
|
|
<div class="ring-label">
|
|
<span class="ring-pct" id="ring-pct">—</span>
|
|
<span class="ring-sub-label">Health</span>
|
|
</div>
|
|
</div>
|
|
<div class="health-stats" id="health-stats">
|
|
<div class="health-stat-row">
|
|
<span class="health-dot" style="background:var(--success)"></span>
|
|
<span class="health-stat-name">Online</span>
|
|
<span class="health-stat-val" id="stat-online">—</span>
|
|
</div>
|
|
<div class="health-stat-row">
|
|
<span class="health-dot" style="background:var(--warning)"></span>
|
|
<span class="health-stat-name">Degraded</span>
|
|
<span class="health-stat-val" id="stat-degraded">—</span>
|
|
</div>
|
|
<div class="health-stat-row">
|
|
<span class="health-dot" style="background:var(--danger)"></span>
|
|
<span class="health-stat-name">Offline</span>
|
|
<span class="health-stat-val" id="stat-offline">—</span>
|
|
</div>
|
|
<div class="health-stat-row">
|
|
<span class="health-dot" style="background:var(--text-tertiary)"></span>
|
|
<span class="health-stat-name">Total</span>
|
|
<span class="health-stat-val">33</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="card">
|
|
<div class="section-header" style="margin-bottom:12px;">
|
|
<span class="section-title">Quick Actions</span>
|
|
</div>
|
|
<div class="qa-grid">
|
|
<button class="qa-btn blue" onclick="qaAction('invite')">
|
|
<span class="qa-icon">👤</span>
|
|
<span class="qa-label">Invite User</span>
|
|
<span class="qa-desc">Add new member</span>
|
|
</button>
|
|
<button class="qa-btn green" onclick="qaAction('deploy')">
|
|
<span class="qa-icon">🚀</span>
|
|
<span class="qa-label">Deploy</span>
|
|
<span class="qa-desc">Push to prod</span>
|
|
</button>
|
|
<button class="qa-btn orange" onclick="qaAction('modules')">
|
|
<span class="qa-icon">🔧</span>
|
|
<span class="qa-label">Modules</span>
|
|
<span class="qa-desc">Manage active</span>
|
|
</button>
|
|
<button class="qa-btn purple" onclick="qaAction('report')">
|
|
<span class="qa-icon">📊</span>
|
|
<span class="qa-label">Report</span>
|
|
<span class="qa-desc">Generate PDF</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── PAGE: Users ── -->
|
|
<section class="page-content" id="page-users">
|
|
<div class="placeholder-page">
|
|
<svg viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
|
|
<h3>User Management</h3>
|
|
<p>User directory, roles and permissions will appear here.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── PAGE: Modules ── -->
|
|
<section class="page-content" id="page-modules">
|
|
<div class="placeholder-page">
|
|
<svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-5 4h2v8h-2V8zm-4 0h2v8h-2V8zM7 8h2v8H7V8z"/></svg>
|
|
<h3>Module Registry</h3>
|
|
<p>All 33 services and their module configurations will appear here.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── PAGE: Settings ── -->
|
|
<section class="page-content" id="page-settings">
|
|
<div class="placeholder-page">
|
|
<svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
|
<h3>Settings</h3>
|
|
<p>System configuration, integrations and preferences will appear here.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── PAGE: Audit ── -->
|
|
<section class="page-content" id="page-audit">
|
|
<div class="placeholder-page">
|
|
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
|
|
<h3>Audit Log</h3>
|
|
<p>Full audit trail with filtering, export and compliance reports.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── PAGE: Onboarding ── -->
|
|
<section class="page-content" id="page-onboarding">
|
|
<div class="placeholder-page">
|
|
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg>
|
|
<h3>Onboarding</h3>
|
|
<p>Guided setup flows, checklists and new user welcome sequences.</p>
|
|
</div>
|
|
</section>
|
|
|
|
</div><!-- /main -->
|
|
|
|
<!-- ──────────────── BOTTOM TAB BAR ───────────────── -->
|
|
<nav class="tab-bar">
|
|
<div class="tab-bar-inner">
|
|
<div class="tab-item active" data-page="dashboard">
|
|
<div class="tab-icon"><svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg></div>
|
|
<span class="tab-label">Dashboard</span>
|
|
</div>
|
|
<div class="tab-item" data-page="users">
|
|
<div class="tab-icon"><svg viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg></div>
|
|
<span class="tab-label">Users</span>
|
|
</div>
|
|
<div class="tab-item" data-page="modules">
|
|
<div class="tab-icon"><svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-5 4h2v8h-2V8zm-4 0h2v8h-2V8zM7 8h2v8H7V8z"/></svg></div>
|
|
<span class="tab-label">Modules</span>
|
|
</div>
|
|
<div class="tab-item" data-page="audit">
|
|
<div class="tab-icon"><svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg></div>
|
|
<span class="tab-label">Audit</span>
|
|
</div>
|
|
<div class="tab-item" data-page="settings">
|
|
<div class="tab-icon"><svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg></div>
|
|
<span class="tab-label">Settings</span>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- ──────────────── TOASTS ────────────────────────── -->
|
|
<div class="toast-container" id="toast-container"></div>
|
|
|
|
<script>
|
|
'use strict';
|
|
|
|
// ── State ──────────────────────────────────────────
|
|
const state = {
|
|
currentPage: 'dashboard',
|
|
lastRefresh: null,
|
|
refreshTimer: null,
|
|
refreshInterval: 30000,
|
|
data: null,
|
|
};
|
|
|
|
// ── Navigation ─────────────────────────────────────
|
|
const pageTitles = {
|
|
dashboard: 'Dashboard',
|
|
users: 'Users',
|
|
modules: 'Modules',
|
|
settings: 'Settings',
|
|
audit: 'Audit Log',
|
|
onboarding:'Onboarding',
|
|
};
|
|
|
|
function navigateTo(page) {
|
|
if (!document.getElementById('page-' + page)) return;
|
|
state.currentPage = page;
|
|
|
|
document.querySelectorAll('.page-content').forEach(el => el.classList.remove('active'));
|
|
document.getElementById('page-' + page).classList.add('active');
|
|
|
|
document.querySelectorAll('.nav-item').forEach(el => {
|
|
el.classList.toggle('active', el.dataset.page === page);
|
|
});
|
|
document.querySelectorAll('.tab-item').forEach(el => {
|
|
el.classList.toggle('active', el.dataset.page === page);
|
|
});
|
|
|
|
document.getElementById('page-title').textContent = pageTitles[page] || page;
|
|
}
|
|
|
|
document.querySelectorAll('[data-page]').forEach(el => {
|
|
el.addEventListener('click', () => navigateTo(el.dataset.page));
|
|
});
|
|
document.querySelectorAll('[data-page-link]').forEach(el => {
|
|
el.addEventListener('click', () => navigateTo(el.dataset.pageLink));
|
|
});
|
|
|
|
// ── Theme ──────────────────────────────────────────
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
|
function applyTheme(dark) {
|
|
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
|
document.getElementById('theme-icon-moon').style.display = dark ? 'none' : '';
|
|
document.getElementById('theme-icon-sun').style.display = dark ? '' : 'none';
|
|
}
|
|
applyTheme(prefersDark.matches);
|
|
prefersDark.addEventListener('change', e => applyTheme(e.matches));
|
|
|
|
['theme-toggle', 'mobile-theme-btn'].forEach(id => {
|
|
const btn = document.getElementById(id);
|
|
if (btn) btn.addEventListener('click', () => {
|
|
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
applyTheme(!isDark);
|
|
});
|
|
});
|
|
|
|
// Show mobile theme btn on small screens
|
|
if (window.innerWidth <= 768) {
|
|
const mb = document.getElementById('mobile-theme-btn');
|
|
if (mb) mb.style.display = '';
|
|
}
|
|
|
|
// ── Toast ──────────────────────────────────────────
|
|
function showToast(msg, type = 'info', duration = 3500) {
|
|
const c = document.getElementById('toast-container');
|
|
const t = document.createElement('div');
|
|
t.className = 'toast ' + type;
|
|
t.textContent = msg;
|
|
c.appendChild(t);
|
|
setTimeout(() => {
|
|
t.classList.add('removing');
|
|
t.addEventListener('animationend', () => t.remove());
|
|
}, duration);
|
|
}
|
|
|
|
// ── Audit icon helper ──────────────────────────────
|
|
const auditIcons = {
|
|
info: `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>`,
|
|
success: `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14l-4-4 1.41-1.41L10 13.17l6.59-6.59L18 8l-8 8z"/></svg>`,
|
|
warning: `<svg viewBox="0 0 24 24"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>`,
|
|
danger: `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>`,
|
|
};
|
|
|
|
function timeAgo(ts) {
|
|
const diff = Math.floor((Date.now() - new Date(ts)) / 1000);
|
|
if (diff < 60) return diff + 's ago';
|
|
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
if (diff < 86400)return Math.floor(diff / 3600) + 'h ago';
|
|
return Math.floor(diff / 86400) + 'd ago';
|
|
}
|
|
|
|
// ── Render helpers ─────────────────────────────────
|
|
function renderKPIs(d) {
|
|
const users = d.total_users ?? d.users?.total ?? '—';
|
|
document.getElementById('kpi-users').textContent = users;
|
|
document.getElementById('kpi-users-sub').innerHTML =
|
|
`<span class="up">↑ ${d.users?.new_today ?? 0} today</span>`;
|
|
|
|
const healthy = d.healthy_services ?? d.services?.healthy ?? 0;
|
|
const total_sv = d.total_services ?? d.services?.total ?? 33;
|
|
const degraded = d.degraded_services ?? d.services?.degraded ?? 0;
|
|
const offline = d.offline_services ?? d.services?.offline ?? 0;
|
|
|
|
document.getElementById('kpi-services').textContent = `${healthy}/${total_sv}`;
|
|
const svcClass = degraded > 0 ? 'warn' : healthy === total_sv ? 'up' : 'down';
|
|
document.getElementById('kpi-services-sub').innerHTML =
|
|
`<span class="${svcClass}">${degraded > 0 ? degraded + ' degraded' : healthy === total_sv ? 'All healthy' : offline + ' offline'}</span>`;
|
|
|
|
const mods = d.active_modules ?? d.modules?.active ?? '—';
|
|
document.getElementById('kpi-modules').textContent = mods;
|
|
document.getElementById('kpi-modules-sub').innerHTML =
|
|
`${d.modules?.total ?? '—'} total installed`;
|
|
|
|
const load = d.system_load ?? d.system?.load ?? '—';
|
|
const loadNum = parseFloat(load);
|
|
document.getElementById('kpi-load').textContent =
|
|
isNaN(loadNum) ? load : loadNum.toFixed(1) + '%';
|
|
const loadClass = loadNum > 80 ? 'down' : loadNum > 60 ? 'warn' : 'up';
|
|
document.getElementById('kpi-load-sub').innerHTML =
|
|
`<span class="${loadClass}">${isNaN(loadNum) ? '' : loadNum > 80 ? 'High' : loadNum > 60 ? 'Moderate' : 'Normal'}</span>`;
|
|
|
|
// Ring
|
|
const pct = total_sv > 0 ? Math.round((healthy / total_sv) * 100) : 0;
|
|
const circ = 2 * Math.PI * 50;
|
|
const offset = circ - (pct / 100) * circ;
|
|
const ring = document.getElementById('ring-path');
|
|
ring.style.strokeDasharray = circ;
|
|
ring.style.strokeDashoffset = offset;
|
|
ring.style.stroke = pct >= 90 ? 'var(--success)' : pct >= 70 ? 'var(--warning)' : 'var(--danger)';
|
|
document.getElementById('ring-pct').textContent = pct + '%';
|
|
document.getElementById('stat-online').textContent = healthy;
|
|
document.getElementById('stat-degraded').textContent = degraded;
|
|
document.getElementById('stat-offline').textContent = offline;
|
|
}
|
|
|
|
function renderAudit(events) {
|
|
const list = document.getElementById('audit-list');
|
|
if (!events || !events.length) {
|
|
list.innerHTML = '<div style="color:var(--text-tertiary);font-size:14px;padding:12px 0;">No recent events</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = events.slice(0, 5).map(ev => {
|
|
const lvl = ev.level || ev.severity || 'info';
|
|
const icon = auditIcons[lvl] || auditIcons.info;
|
|
const ts = ev.timestamp || ev.created_at || ev.time;
|
|
return `
|
|
<div class="audit-row">
|
|
<div class="audit-icon ${lvl}">${icon}</div>
|
|
<div class="audit-body">
|
|
<div class="audit-event">${escHtml(ev.event || ev.message || ev.action || 'Unknown event')}</div>
|
|
<div class="audit-meta">${escHtml(ev.user || ev.actor || '')}${ev.resource ? ' · ' + escHtml(ev.resource) : ''}</div>
|
|
</div>
|
|
<div class="audit-time">${ts ? timeAgo(ts) : ''}</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderServices(services) {
|
|
const tbody = document.getElementById('service-tbody');
|
|
if (!services || !services.length) {
|
|
tbody.innerHTML = '<tr><td colspan="4" style="color:var(--text-tertiary);padding:12px 0;">No data</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = services.slice(0, 8).map(s => {
|
|
const status = s.status || 'unknown';
|
|
const pillClass = status === 'ok' || status === 'healthy' || status === 'up'
|
|
? 'ok' : status === 'degraded' || status === 'warn' ? 'warn' : 'error';
|
|
const pillLabel = pillClass === 'ok' ? 'Online' : pillClass === 'warn' ? 'Degraded' : 'Offline';
|
|
const latency = s.latency_ms != null ? s.latency_ms + ' ms' : s.latency || '—';
|
|
const uptime = s.uptime_pct != null ? s.uptime_pct.toFixed(2) + '%' : s.uptime || '—';
|
|
return `<tr>
|
|
<td class="svc-name">${escHtml(s.name || s.service || '—')}</td>
|
|
<td><span class="status-pill ${pillClass}">${pillLabel}</span></td>
|
|
<td class="svc-latency">${escHtml(String(latency))}</td>
|
|
<td class="svc-latency">${escHtml(String(uptime))}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
function escHtml(str) {
|
|
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// ── Fallback demo data ─────────────────────────────
|
|
function demoData() {
|
|
const now = Date.now();
|
|
return {
|
|
total_users: 1284,
|
|
users: { total: 1284, new_today: 7 },
|
|
healthy_services: 29,
|
|
degraded_services: 3,
|
|
offline_services: 1,
|
|
total_services: 33,
|
|
services: { healthy: 29, degraded: 3, offline: 1, total: 33 },
|
|
active_modules: 18,
|
|
modules: { active: 18, total: 24 },
|
|
system_load: 42.7,
|
|
system: { load: 42.7 },
|
|
audit_events: [
|
|
{ event: 'User login: admin@aamos.io', user: 'admin@aamos.io', level: 'info', timestamp: new Date(now - 120000).toISOString() },
|
|
{ event: 'Module "ResearchPipeline" enabled', user: 'ops@aamos.io', level: 'success', timestamp: new Date(now - 540000).toISOString() },
|
|
{ event: 'Service "workers-monitor" degraded', user: 'system', level: 'warning', timestamp: new Date(now - 900000).toISOString() },
|
|
{ event: 'Failed login attempt: unknown@evil.io', user: 'unknown', level: 'danger', timestamp: new Date(now - 1800000).toISOString() },
|
|
{ event: 'Config updated: rate_limit=500', user: 'bernt@aamos.io', level: 'info', timestamp: new Date(now - 3600000).toISOString() },
|
|
],
|
|
service_health: [
|
|
{ name: 'api-gateway', status: 'ok', latency_ms: 12, uptime_pct: 99.98 },
|
|
{ name: 'auth-service', status: 'ok', latency_ms: 8, uptime_pct: 99.99 },
|
|
{ name: 'research-pipeline',status: 'ok', latency_ms: 145, uptime_pct: 99.87 },
|
|
{ name: 'workers-monitor', status: 'degraded', latency_ms: 890, uptime_pct: 98.12 },
|
|
{ name: 'digest-engine', status: 'ok', latency_ms: 34, uptime_pct: 99.95 },
|
|
{ name: 'mail-relay', status: 'ok', latency_ms: 22, uptime_pct: 99.94 },
|
|
{ name: 'build-monitor', status: 'degraded', latency_ms: 430, uptime_pct: 97.60 },
|
|
{ name: 'wavult-core', status: 'error', latency_ms: null, uptime_pct: 0 },
|
|
],
|
|
};
|
|
}
|
|
|
|
// ── Fetch & render ─────────────────────────────────
|
|
async function fetchDashboard(silent = false) {
|
|
const dot = document.getElementById('refresh-dot');
|
|
const lbl = document.getElementById('refresh-label');
|
|
dot.classList.add('loading');
|
|
if (!silent) lbl.textContent = 'Loading…';
|
|
|
|
try {
|
|
const resp = await fetch('/api/v1/dashboard', {
|
|
headers: { 'Accept': 'application/json' },
|
|
signal: AbortSignal.timeout(8000),
|
|
});
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
const data = await resp.json();
|
|
state.data = data;
|
|
renderAll(data);
|
|
state.lastRefresh = new Date();
|
|
lbl.textContent = 'Live';
|
|
dot.classList.remove('loading');
|
|
} catch (err) {
|
|
dot.classList.remove('loading');
|
|
dot.style.background = 'var(--warning)';
|
|
lbl.textContent = 'Offline';
|
|
if (!silent) {
|
|
const demo = demoData();
|
|
renderAll(demo);
|
|
showToast('API unavailable — showing demo data', 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderAll(data) {
|
|
renderKPIs(data);
|
|
renderAudit(data.audit_events || data.events || []);
|
|
renderServices(data.service_health || data.services_list || data.services_health || []);
|
|
}
|
|
|
|
// ── Auto refresh ───────────────────────────────────
|
|
function scheduleRefresh() {
|
|
clearInterval(state.refreshTimer);
|
|
state.refreshTimer = setInterval(() => {
|
|
fetchDashboard(true);
|
|
}, state.refreshInterval);
|
|
}
|
|
|
|
document.getElementById('refresh-btn').addEventListener('click', () => {
|
|
fetchDashboard(false);
|
|
scheduleRefresh();
|
|
});
|
|
|
|
// ── Quick Actions ──────────────────────────────────
|
|
function qaAction(action) {
|
|
const msgs = {
|
|
invite: 'Opening invite user flow…',
|
|
deploy: 'Triggering deployment pipeline…',
|
|
modules: 'Opening module manager…',
|
|
report: 'Generating PDF report…',
|
|
};
|
|
showToast(msgs[action] || 'Action: ' + action, 'info');
|
|
if (action === 'modules') navigateTo('modules');
|
|
}
|
|
|
|
// ── Init ───────────────────────────────────────────
|
|
fetchDashboard(false);
|
|
scheduleRefresh();
|
|
|
|
// Visibility API: refresh when tab becomes active after being hidden
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (!document.hidden) {
|
|
const stale = !state.lastRefresh || (Date.now() - state.lastRefresh) > state.refreshInterval;
|
|
if (stale) fetchDashboard(true);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|