Skip to content
Merged
24 changes: 21 additions & 3 deletions public/css/aurora.css
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ a{color:inherit}

/* sidebar (production: .sidebar, .nav-item, .nav-section-label, .nav-badge) */
.sidebar{grid-area:side; border-right:1px solid var(--line); padding:16px 12px 24px; overflow-y:auto;
display:flex; flex-direction:column; gap:3px; background:linear-gradient(180deg,var(--surface),transparent 40%)}
display:flex; flex-direction:column; gap:3px; background:linear-gradient(180deg,var(--surface),transparent 40%); position:static}
.nav-section-label{font-size:10.5px; text-transform:uppercase; letter-spacing:.1em; color:var(--faint); font-weight:700; padding:14px 12px 6px}
.nav-section-label:first-child{padding-top:2px}
.nav-item{display:flex; align-items:center; gap:11px; padding:9px 12px; border-radius:10px; cursor:pointer;
Expand Down Expand Up @@ -444,6 +444,11 @@ input[type=date]:focus, select:focus {
.pi-stats .s .n{font-family:var(--font-mono); font-weight:700; font-size:19px}
.pi-stats .s .n.blk{color:var(--coral)} .pi-stats .s .t{color:var(--muted); font-size:13px}

/* resource donut gauges — NEW (aurora): CPU + RAM radial donuts on dashboard */
.res-gauge-wrap{display:flex; gap:18px; align-items:center; margin-top:8px; flex-wrap:wrap}
.res-gauge-info{display:flex; flex-direction:column; justify-content:center; flex:1; min-width:0}
.res-gauge-info .ri{font-family:var(--font-mono); font-size:11px; color:var(--muted); word-break:break-word}

.toplist{margin-top:16px; border-top:1px solid var(--line); padding-top:13px}
.toplist .h{font-size:11px; color:var(--faint); text-transform:uppercase; letter-spacing:.08em; margin-bottom:9px}
.toplist ul{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:8px}
Expand Down Expand Up @@ -574,15 +579,24 @@ input[type=date]:focus, select:focus {
.gw-relnotes{font-size:12px;color:var(--muted);text-decoration:underline;text-underline-offset:2px;opacity:.8}
.gw-relnotes:hover{opacity:1;color:var(--text)}

/* Issue 11: Gateway detail — 3-column (1/3 each) sub-card grid */
.gw-detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
.gw-detail-grid .gw.full{grid-column:1/-1}
/* Fix 3: "Discovered devices" + "Scan targets" at 1/2 width each in their own row */
.gw-half-row{display:grid;grid-template-columns:1fr 1fr;gap:14px;grid-column:1/-1}
@media(max-width:640px){.gw-half-row{grid-template-columns:1fr}}

/* ----------------------------------------------------------------------------
C · PEERS page — Aurora mockup fidelity (Task P2-3)
--------------------------------------------------------------------------- */

/* Empty-state for the gateway unit-grid */
.aurora-gw-empty{padding:24px;text-align:center;color:var(--muted);font-size:13px}

/* tag-dot helper used in Aurora status tags on gateway/peer rows */
.tag.tag-dot::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:currentColor;margin-right:5px;vertical-align:middle}
/* tag-dot helper used in Aurora status tags on gateway/peer rows.
Issue 9: text LEFT of dot — suppress ::before, put dot via ::after */
.tag.tag-dot::before{content:none}
.tag.tag-dot::after{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:currentColor;margin-left:5px}

/* ----------------------------------------------------------------------------
D · ROUTES page — Wizard modal shells + step progress (Task P2-4b)
Expand Down Expand Up @@ -719,6 +733,8 @@ input[type=date]:focus, select:focus {
.modal-foot.wiz-foot{padding:16px 24px; border-top:1px solid var(--line); display:flex; align-items:center; justify-content:space-between; gap:8px; position:sticky; bottom:0; background:var(--surface)}

/* Aurora RDP card grid (built by auroraRenderGrid()) */
/* Issue 12: auto-fill minmax grid — matches .unit-grid sizing on gateways/peers pages */
.rdp-card-grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:14px}
.rdp-aurora-card-actions{display:flex; gap:6px; align-items:center; flex-wrap:wrap; margin-top:12px}

/* ── Settings page — Aurora mockup fidelity (Task P2-11) ─────────────────────── */
Expand All @@ -739,6 +755,8 @@ input[type=date]:focus, select:focus {

/* Panel (moved from settings.njk <style nonce> block) */
.settings-panel{display:flex; flex-direction:column; gap:14px}
/* Cards size to own content within settings grids — prevents app-info from stretching to match tall neighbours */
.settings-panel .grid{align-items:start}

/* Mobile hamburger (moved from settings.njk <style nonce> block) */
.settings-tab-toggle{
Expand Down
95 changes: 83 additions & 12 deletions public/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,12 @@
let v = Number(bytes);
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
const val = v < 10 ? v.toFixed(1) : Math.round(v);
return `${val}<span style="font-size:14px;font-weight:400"> ${units[i]}</span>`;

Check warning on line 106 in public/js/dashboard.js

View workflow job for this annotation

GitHub Actions / ESLint Security

Generic Object Injection Sink

Check warning on line 106 in public/js/dashboard.js

View workflow job for this annotation

GitHub Actions / ESLint Security

Generic Object Injection Sink
}

// ─── System Resources ───────────────────────────────────────────────────────
async function refreshResources() {
if (isAurora()) return auroraRefreshResources();
try {
const data = await api.get('/api/system/resources');

Expand Down Expand Up @@ -209,7 +210,7 @@
const step = w / Math.max(1, points.length - 1);
return points.map((p, i) => {
const x = i * step;
const y = h - 10 - ((p[key] / maxVal) * (h - 20));

Check warning on line 213 in public/js/dashboard.js

View workflow job for this annotation

GitHub Actions / ESLint Security

Generic Object Injection Sink

Check warning on line 213 in public/js/dashboard.js

View workflow job for this annotation

GitHub Actions / ESLint Security

Generic Object Injection Sink
return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
}
Expand Down Expand Up @@ -255,7 +256,7 @@
}

// ─── Auto-update status (header) ────────────────────────────────────────────
function T(k, d) { return (window.GC && GC.t && GC.t[k]) || d; }

Check warning on line 259 in public/js/dashboard.js

View workflow job for this annotation

GitHub Actions / ESLint Security

Generic Object Injection Sink

Check warning on line 259 in public/js/dashboard.js

View workflow job for this annotation

GitHub Actions / ESLint Security

Generic Object Injection Sink

function auAgo(s) { if (s == null) return '—'; return s < 60 ? T('autoupdate.ago_seconds','{x}s').replace('{x}', s) : T('autoupdate.ago_minutes','{x}m').replace('{x}', Math.round(s/60)); }

Expand Down Expand Up @@ -394,12 +395,13 @@
// The original refreshStats/refreshActivity/renderChart else-paths remain byte-identical.

async function auroraRefreshStats() {
var peerOnlineCount = 0; // Bug 2: captured here, applied after gateways fetch

try {
const data = await api.get('/api/dashboard/stats');

// Peers connected
var peersEl = document.getElementById('stat-peers');
if (peersEl) peersEl.textContent = data.peers.online;
// Bug 2: save count; will be adjusted after gateway subtraction below
peerOnlineCount = (data.peers && typeof data.peers.online === 'number') ? data.peers.online : 0;

// Active routes
var routesEl = document.getElementById('stat-routes');
Expand Down Expand Up @@ -458,18 +460,23 @@
console.error('Aurora: Failed to refresh stats:', err);
}

// Separate fetch for gateways KPI (not in /api/dashboard/stats)
// Separate fetch for gateways KPI (Bug 1: API returns { gateways:[…] }, not a bare array)
var onlineGateways = 0;
try {
var gwData = await api.get('/api/v1/gateways');
var gwEl = document.getElementById('stat-gateways');
if (gwEl && gwData && Array.isArray(gwData)) {
var online = gwData.filter(function(g) { return g.status === 'online'; }).length;
gwEl.textContent = online + '/' + gwData.length;
if (gwData && Array.isArray(gwData.gateways)) {
onlineGateways = gwData.gateways.filter(function(g) { return g.status === 'online'; }).length;
if (gwEl) gwEl.textContent = onlineGateways + '/' + gwData.gateways.length;
}
} catch (err) {
// non-fatal — gateways KPI stays at '—'
}

// Peers KPI: subtract online gateways so only real (non-gateway) peers are shown (Bug 2)
var peersEl = document.getElementById('stat-peers');
if (peersEl) peersEl.textContent = Math.max(0, peerOnlineCount - onlineGateways);

// Pi-hole donut (3-state: no card if gated off; donut on ok; empty-state on error)
var donutCard = document.getElementById('pihole-donut-card');
if (donutCard) {
Expand All @@ -485,8 +492,9 @@
if (!donut) return;

try {
var ph = await api.get('/api/pihole/stats');
var pct = (ph && ph.summary && ph.summary.queries && ph.summary.queries.percent) || 0;
// Bug 3: correct URL (/api/v1/pihole/summary) and response keys (ph.data.*)
var ph = await api.get('/api/v1/pihole/summary');
var pct = (ph && ph.data && ph.data.queries && ph.data.queries.percent) || 0;
var pctRounded = Math.round(pct * 10) / 10;

// Animate the donut arc
Expand All @@ -500,9 +508,9 @@

// Pi-hole stats body
if (statsBody) {
var blocked = (ph.summary && ph.summary.queries && ph.summary.queries.blocked) || 0;
var totalQ = (ph.summary && ph.summary.queries && ph.summary.queries.total) || 0;
var gravity = (ph.summary && ph.summary.gravity) || 0;
var blocked = (ph.data && ph.data.queries && ph.data.queries.blocked) || 0;
var totalQ = (ph.data && ph.data.queries && ph.data.queries.total) || 0;
var gravity = (ph.data && ph.data.gravity) || 0;
statsBody.innerHTML =
'<div class="s"><span class="n blk">' + blocked.toLocaleString() + '</span><span class="t">' + T('dashboard.pihole_blocked', 'Blocked') + '</span></div>' +
'<div class="s"><span class="n">' + totalQ.toLocaleString() + '</span><span class="t">' + T('dashboard.pihole_total_queries', 'Total queries') + '</span></div>' +
Expand All @@ -522,6 +530,69 @@
}
}

// Enhancement 4: Aurora resource donut gauge update
async function auroraRefreshResources() {
try {
var data = await api.get('/api/system/resources');
var cpuPct = data.cpu.percent;
var ramPct = data.memory.percent;

// Write shared IDs (shared refreshResources() bypassed for aurora; we write here)
var cpuPctEl = document.getElementById('cpu-pct');
if (cpuPctEl) cpuPctEl.textContent = cpuPct + ' %';

var cpuBar = document.getElementById('cpu-bar');
if (cpuBar) {
cpuBar.style.width = cpuPct + '%';
cpuBar.style.background = cpuPct > 80 ? 'var(--red)' :
cpuPct > 50 ? 'var(--amber)' : 'var(--green)';
}

var cpuInfo = document.getElementById('cpu-info');
if (cpuInfo) cpuInfo.textContent = data.cpu.cores + ' Cores · ' + data.cpu.model.split(' ').slice(0, 3).join(' ');

var ramPctEl = document.getElementById('ram-pct');
if (ramPctEl) ramPctEl.textContent = ramPct + ' %';

var ramBar = document.getElementById('ram-bar');
if (ramBar) {
ramBar.style.width = ramPct + '%';
ramBar.style.background = ramPct > 90 ? 'var(--red)' :
ramPct > 70 ? 'var(--amber)' : 'var(--blue)';
}

var ramInfo = document.getElementById('ram-info');
if (ramInfo) ramInfo.textContent = formatBytes(data.memory.used) + ' / ' + formatBytes(data.memory.total);

var uptimeValue = document.getElementById('uptime-value');
if (uptimeValue) uptimeValue.textContent = data.uptime.formatted;

var uptimeBoot = document.getElementById('uptime-boot');
if (uptimeBoot && data.uptime && data.uptime.bootTime) {
var label = (GC.t && GC.t['dashboard.booted_on']) || 'Seit {date}';
uptimeBoot.textContent = label.replace('{date}', data.uptime.bootTime);
}

// Update radial donut gauge arcs
auroraSetResourceDonut('cpu-donut', cpuPct);
auroraSetResourceDonut('ram-donut', ramPct);

} catch (err) {
console.error('Aurora: Failed to refresh resources:', err);
}
}

// Set stroke-dasharray + color on a resource donut arc by load percentage
function auroraSetResourceDonut(donutId, pct) {
var donut = document.getElementById(donutId);
if (!donut) return;
var arc = donut.querySelector('.val');
if (!arc) return;
var p = Math.min(100, Math.max(0, Number(pct) || 0));
arc.setAttribute('stroke-dasharray', p + ' ' + (100 - p));
arc.style.stroke = p > 90 ? 'var(--red)' : p > 70 ? 'var(--amber)' : 'var(--teal)';
}

async function auroraRefreshActivity() {
try {
var data = await api.get('/api/logs/recent?limit=8');
Expand Down
49 changes: 43 additions & 6 deletions public/js/gateways.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,11 @@
nameWrap.appendChild(el('div', 'ud', (t.gateway_version || '—') + (g.hostname ? ' · ' + g.hostname : '')));
uh.appendChild(nameWrap);
var tagCls = 'tag ' + (st === 'online' ? 'tag-green' : st === 'degraded' ? 'tag-amber' : 'tag-grey') + ' tag-dot';
var stTag = el('span', tagCls, T('gateways.' + st, st)); stTag.style.marginLeft = 'auto';
uh.appendChild(stTag);
var stTag = el('span', tagCls, T('gateways.' + st, st));
// Issue 8: badge INSIDE card header — wrap in right container pushed to far-right
var right = el('span'); right.style.cssText = 'margin-left:auto;display:inline-flex;align-items:center;gap:6px;flex-shrink:0';
right.appendChild(stTag);
uh.appendChild(right);
wrap.appendChild(uh);
// Body: resource bars (online/degraded) or empty-state (offline)
if (st === 'offline') {
Expand All @@ -97,18 +100,52 @@
wrap.appendChild(btn);
return wrap;
}
// Issue 10: Aurora-specific versions card — vertical kv list to prevent overflow
function auroraVersionsCard(g) {
var t = (g.health && g.health.telemetry) || {}, h = g.health || {};
var c = el('div', 'gw');
var top = el('div', 'top'); top.appendChild(el('h3', null, T('gateways.sec_versions', 'Versionen & System'))); c.appendChild(top);
var body = el('div', 'body');
function addKv(k, v, vcls) { body.appendChild(kvRow(k, v, vcls)); }
var gwVal = el('span'); gwVal.appendChild(document.createTextNode((t.gateway_version || '—') + ' '));
if (g.update_available && latest) gwVal.appendChild(el('span', 'badge drift', '↑ ' + latest));
addKv(T('gateways.lbl_gateway', 'Gateway'), gwVal);
addKv(T('gateways.lbl_node', 'Node'), t.node_version || '—');
addKv(T('gateways.lbl_wgtools', 'wg-tools'), t.wg_tools_version || '—');
addKv(T('gateways.lbl_os', 'OS'), (t.os_platform || '—') + (t.os_release ? ' ' + t.os_release : ''));
addKv(T('gateways.lbl_arch', 'Arch'), t.arch || '—');
addKv(T('gateways.lbl_cores', 'Cores'), t.cpu_cores != null ? String(t.cpu_cores) : '—');
addKv(T('gateways.lbl_default_gw', 'Default gateway (LAN)'), t.default_gateway_ip || '—');
addKv(T('gateways.lbl_dns_resolvers', 'DNS resolvers'), (t.dns_resolvers && t.dns_resolvers.length) ? t.dns_resolvers.join(', ') : '—');
var cfgVal;
if (h.config_hash) {
cfgVal = el('span'); cfgVal.appendChild(document.createTextNode('✓ ' + T('gateways.config_synced', 'synchron') + ' · '));
cfgVal.appendChild(el('code', null, String(h.config_hash).slice(0, 8)));
} else { cfgVal = T('gateways.config_unknown', 'unbekannt'); }
addKv(T('gateways.lbl_config_hash', 'Config hash'), cfgVal);
var shortDigest = '—';
if (t.image_digest) { var di = String(t.image_digest); var at = di.lastIndexOf('@sha256:'); if (at !== -1) di = di.slice(at + 8); shortDigest = di.slice(-12); }
addKv(T('gateways.lbl_image_digest', 'Image'), shortDigest);
addKv(T('gateways.lbl_last_pull', 'Last pull'), t.last_pull_at ? ago(t.last_pull_at) : T('gateways.last_pull_never', 'never'));
c.appendChild(body);
return c;
}
function auroraRenderDetail(g) {
var root = el('div', 'gw-detail');
var back = el('button', 'gw-back', '← ' + T('gateways.back_to_fleet', 'Back to fleet')); back.dataset.act = 'back';
root.appendChild(back);
root.appendChild(detailHead(g));
var grid2 = el('div', 'unit-grid');
grid2.appendChild(versionsCard(g));
// Issue 11: 3-column detail grid (1/3 width per card); auroraVersionsCard for Issue 10
var grid2 = el('div', 'gw-detail-grid');
grid2.appendChild(auroraVersionsCard(g));
grid2.appendChild(resourcesCard(g));
grid2.appendChild(routesCard(g));
grid2.appendChild(discoveredDevicesCard(g));
// Bottom two cards ("Discovered devices" + "Scan targets") at 1/2 width each
var telE = (g.health && g.health.telemetry) || {};
if (telE.scan_egress === true) grid2.appendChild(egressCard(g));
var halfRow = el('div', 'gw-half-row');
halfRow.appendChild(discoveredDevicesCard(g));
if (telE.scan_egress === true) halfRow.appendChild(egressCard(g));
grid2.appendChild(halfRow);
root.appendChild(grid2);
detailView.replaceChildren(root);
}
Expand Down
Loading
Loading