From 3dd10eae60794d7f138ff2019aa21bb85d3a9ee9 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:55:00 +0200 Subject: [PATCH 1/7] =?UTF-8?q?fix(theme):=20Aurora=20dashboard=20?= =?UTF-8?q?=E2=80=94=20gateway/peer=20counts,=20pihole=20donut,=20CPU/RAM?= =?UTF-8?q?=20gauges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1: /api/v1/gateways returns { gateways:[…] } not a bare array — fix Array.isArray check to use gwData.gateways so stat-gateways shows correctly. Bug 2: Peers KPI included gateway peers — subtract onlineGateways after the gateways fetch resolves so stat-peers shows only non-gateway connected peers. Bug 3: Pi-hole donut fetched wrong URL (/api/pihole/stats) and read wrong response keys (ph.summary.*) — fix to /api/v1/pihole/summary + ph.data.*. Enhancement: CPU + RAM rendered as radial donut gauges (same SVG pattern as pi-hole donut). auroraRefreshResources() + auroraSetResourceDonut() added; refreshResources() gets isAurora guard; dashboard.njk updated with donut SVGs; aurora.css gets .res-gauge-wrap/.res-gauge-info rules. Default/pro unchanged. --- public/css/aurora.css | 5 ++ public/js/dashboard.js | 95 ++++++++++++++++++++++++---- templates/aurora/pages/dashboard.njk | 52 +++++++++++---- tests/aurora_theme.test.js | 43 +++++++++++++ 4 files changed, 169 insertions(+), 26 deletions(-) diff --git a/public/css/aurora.css b/public/css/aurora.css index b291c081..a6360e36 100644 --- a/public/css/aurora.css +++ b/public/css/aurora.css @@ -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} diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 7b8c4831..51a87569 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -108,6 +108,7 @@ // ─── System Resources ─────────────────────────────────────────────────────── async function refreshResources() { + if (isAurora()) return auroraRefreshResources(); try { const data = await api.get('/api/system/resources'); @@ -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'); @@ -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) { @@ -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 @@ -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 = '
' + blocked.toLocaleString() + '' + T('dashboard.pihole_blocked', 'Blocked') + '
' + '
' + totalQ.toLocaleString() + '' + T('dashboard.pihole_total_queries', 'Total queries') + '
' + @@ -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'); diff --git a/templates/aurora/pages/dashboard.njk b/templates/aurora/pages/dashboard.njk index 2d5ec12a..af9a7f32 100644 --- a/templates/aurora/pages/dashboard.njk +++ b/templates/aurora/pages/dashboard.njk @@ -230,27 +230,51 @@
- +
-
- {{ t('dashboard.cpu_usage') }} - -
+
{{ t('dashboard.cpu_usage') }}
-
-
+
+
+ + + + +
+
+
+
+
+
+
+
+ +
- +
-
- {{ t('dashboard.ram_usage') }} - -
+
{{ t('dashboard.ram_usage') }}
-
-
+
+
+ + + + +
+
+
+
+
+
+
+
+ +
diff --git a/tests/aurora_theme.test.js b/tests/aurora_theme.test.js index 364a9e51..3a88a761 100644 --- a/tests/aurora_theme.test.js +++ b/tests/aurora_theme.test.js @@ -1334,3 +1334,46 @@ describe('aurora theme — profile layout (Task P2-12)', () => { assert.ok(de['profile.security_display'], 'profile.security_display present in de.json'); }); }); + +// ── UX-fixes: Dashboard donut gauges + bug fixes ───────────────────────────── +describe('aurora theme — dashboard UX fixes (ux-dash)', () => { + it('dashboard.njk has #cpu-donut and #ram-donut SVG elements', async () => { + selectAurora(); + const res = await agent.get('/dashboard').expect(200); + assert.match(res.text, /id="cpu-donut"/, '#cpu-donut SVG present in aurora dashboard'); + assert.match(res.text, /id="ram-donut"/, '#ram-donut SVG present in aurora dashboard'); + // Both donuts must contain the .val arc circle + assert.match(res.text, /id="cpu-donut"[\s\S]{0,400}class="val"/, '#cpu-donut has .val arc'); + assert.match(res.text, /id="ram-donut"[\s\S]{0,400}class="val"/, '#ram-donut has .val arc'); + }); + + it('dashboard.njk still has all required resource IDs after donut redesign', async () => { + selectAurora(); + const res = await agent.get('/dashboard').expect(200); + assert.match(res.text, /id="cpu-pct"/, '#cpu-pct present inside donut center'); + assert.match(res.text, /id="cpu-info"/, '#cpu-info present'); + assert.match(res.text, /id="cpu-bar"/, '#cpu-bar present (hidden, for JS contract)'); + assert.match(res.text, /id="ram-pct"/, '#ram-pct present inside donut center'); + assert.match(res.text, /id="ram-info"/, '#ram-info present'); + assert.match(res.text, /id="ram-bar"/, '#ram-bar present (hidden, for JS contract)'); + }); + + it('dashboard.js uses /api/v1/pihole/summary (not the wrong /api/pihole/stats)', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'dashboard.js'), 'utf8'); + assert.match(js, /\/api\/v1\/pihole\/summary/, 'dashboard.js fetches /api/v1/pihole/summary'); + assert.doesNotMatch(js, /\/api\/pihole\/stats/, '/api/pihole/stats (wrong URL) absent'); + }); + + it('dashboard.js has auroraRefreshResources() and auroraSetResourceDonut() functions', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'dashboard.js'), 'utf8'); + assert.match(js, /function auroraRefreshResources\(/, 'auroraRefreshResources() present'); + assert.match(js, /function auroraSetResourceDonut\(/, 'auroraSetResourceDonut() present'); + assert.match(js, /if \(isAurora\(\)\) return auroraRefreshResources/, 'refreshResources() has isAurora guard'); + }); + + it('aurora.css has .res-gauge-wrap and .res-gauge-info rules', () => { + const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'aurora.css'), 'utf8'); + assert.match(css, /\.res-gauge-wrap\b/, '.res-gauge-wrap rule in aurora.css'); + assert.match(css, /\.res-gauge-info\b/, '.res-gauge-info rule in aurora.css'); + }); +}); From ecf4b86bd74b7f5cd354f4d0cf4813500d59a514 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:07:40 +0200 Subject: [PATCH 2/7] =?UTF-8?q?fix(theme):=20Aurora=20peers=20gateway=20ca?= =?UTF-8?q?rds=20=E2=80=94=20badge=20inside,=20gear-edit,=20card=E2=86=92d?= =?UTF-8?q?etail=20nav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/peers.js | 105 +++++++++++++++++++++++++++++++------ src/i18n/de.json | 1 + src/i18n/en.json | 1 + tests/aurora_theme.test.js | 57 ++++++++++++++++++++ 4 files changed, 148 insertions(+), 16 deletions(-) diff --git a/public/js/peers.js b/public/js/peers.js index e4e22fc6..89f030ed 100644 --- a/public/js/peers.js +++ b/public/js/peers.js @@ -2264,37 +2264,110 @@ } function auroraRenderGatewayCard(gw) { + // ── Issue 5: badge INSIDE the card header ────────────────────────────── + // ── Issue 6: gear button opens edit modal (not card click) ───────────── + // ── Issue 7: card click → /gateways#gw/ detail page ─────────────── var h = gw.health || {}; var isOnline = gw.status === 'online'; - var statusTag = isOnline - ? '' + escapeHtml(gwT('peers.gateway.status_online', 'Online')) + '' - : '' + escapeHtml(gwT('peers.gateway.status_offline', 'Offline')) + ''; + var statusClass = isOnline ? 'tag tag-green tag-dot' : 'tag tag-grey tag-dot'; + var statusLabel = isOnline + ? gwT('peers.gateway.status_online', 'Online') + : gwT('peers.gateway.status_offline', 'Offline'); // Use h.wg_handshake_age_s directly since formatRelTime expects a past timestamp var handshakeText = (typeof h.wg_handshake_age_s === 'number') ? (h.wg_handshake_age_s < 60 ? h.wg_handshake_age_s + 's' : Math.floor(h.wg_handshake_age_s / 60) + 'm') + ' ago' : '—'; var t = h.telemetry || {}; - var rx = formatBytes((t.wg_rx_bytes || 0)); - var tx = formatBytes((t.wg_tx_bytes || 0)); + var rx = formatBytes(t.wg_rx_bytes || 0); + var tx = formatBytes(t.wg_tx_bytes || 0); var trafficText = '↓' + rx + ' ↑' + tx; var unit = document.createElement('div'); unit.className = 'unit'; unit.style.cursor = 'pointer'; - unit.innerHTML = - '
' + - '' + - '' + - '' + - '
' + escapeHtml(gw.name) + '
' + escapeHtml(gw.ip || '') + '
' + - '' + statusTag + '' + - '
' + - '
' + escapeHtml(gwT('peers.gateway.wg_handshake', 'WG-Handshake')) + '' + escapeHtml(handshakeText) + '
' + - '
' + escapeHtml(gwT('peers.traffic', 'Traffic')) + '' + escapeHtml(trafficText) + '
'; + // data-gw-detail carries the target URL for the card-click nav (Issue 7) + unit.dataset.gwDetail = '/gateways#gw/' + gw.peer_id; + // ── Card header row (.uh) ────────────────────────────────────────────── + var uh = document.createElement('div'); + uh.className = 'uh'; + + // Avatar icon + var uav = document.createElement('span'); + uav.className = 'uav'; + uav.style.background = 'linear-gradient(145deg,var(--teal),var(--teal-dim,#28b3a2))'; + uav.innerHTML = ''; + uh.appendChild(uav); + + // Name + IP + var identity = document.createElement('div'); + var un = document.createElement('div'); + un.className = 'un'; + un.textContent = gw.name; + var ud = document.createElement('div'); + ud.className = 'ud'; + ud.textContent = gw.ip || ''; + identity.appendChild(un); + identity.appendChild(ud); + uh.appendChild(identity); + + // Right side: status badge + gear button (pushed right by margin-left:auto) + var right = document.createElement('span'); + right.style.cssText = 'margin-left:auto;display:flex;align-items:center;gap:6px;flex-shrink:0'; + + // Issue 5 — badge INSIDE the card, anchored top-right within header row + var badge = document.createElement('span'); + badge.className = statusClass; + badge.textContent = statusLabel; + right.appendChild(badge); + + // Issue 6 — gear button triggers edit modal; stops propagation so card-click + // doesn't also fire (Issue 7 nav). + var gearBtn = document.createElement('button'); + gearBtn.type = 'button'; + gearBtn.className = 'icon-action'; + gearBtn.setAttribute('data-action', 'edit'); + gearBtn.setAttribute('data-id', String(gw.peer_id)); + var gearLabel = gwT('peers.gateway.action_edit_gear', 'Edit gateway'); + gearBtn.setAttribute('aria-label', gearLabel); + gearBtn.title = gearLabel; + // Gear / cog icon (Lucide settings) + gearBtn.innerHTML = ''; + gearBtn.addEventListener('click', function(e) { + e.stopPropagation(); + showEditModal(gw.peer_id); + }); + right.appendChild(gearBtn); + + uh.appendChild(right); + unit.appendChild(uh); + + // Metric rows + var row1 = document.createElement('div'); + row1.className = 'urow'; + var r1k = document.createElement('span'); + r1k.textContent = gwT('peers.gateway.wg_handshake', 'WG-Handshake'); + var r1v = document.createElement('b'); + r1v.textContent = handshakeText; + row1.appendChild(r1k); + row1.appendChild(r1v); + unit.appendChild(row1); + + var row2 = document.createElement('div'); + row2.className = 'urow'; + var r2k = document.createElement('span'); + r2k.textContent = gwT('peers.traffic', 'Traffic'); + var r2v = document.createElement('b'); + r2v.textContent = trafficText; + row2.appendChild(r2k); + row2.appendChild(r2v); + unit.appendChild(row2); + + // Issue 7 — card click navigates to the gateway detail page. + // Gear button and badge stop propagation so they don't also navigate. unit.addEventListener('click', function(e) { if (e.target.closest('button, a')) return; - showEditModal(gw.peer_id); + window.location.href = '/gateways#gw/' + gw.peer_id; }); return unit; } diff --git a/src/i18n/de.json b/src/i18n/de.json index 65ac33f0..8b8003b4 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -166,6 +166,7 @@ "peers.gateway.none": "Kein Gateway konfiguriert. Neuen Gateway-Peer über „Hinzufügen\" anlegen.", "peers.gateway.version_chip": "Gateway-Container-Version", "peers.gateway.action_edit": "Bearbeiten", + "peers.gateway.action_edit_gear": "Gateway bearbeiten", "peers.gateway.action_env": "Pairing-Tokens anzeigen", "peers.gateway.action_delete": "Gateway löschen", "peers.tags": "Tags", diff --git a/src/i18n/en.json b/src/i18n/en.json index b2520f91..c84ab7bf 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -166,6 +166,7 @@ "peers.gateway.none": "No gateway configured. Add one via the Add button.", "peers.gateway.version_chip": "Gateway container version", "peers.gateway.action_edit": "Edit", + "peers.gateway.action_edit_gear": "Edit gateway", "peers.gateway.action_env": "Pairing tokens", "peers.gateway.action_delete": "Delete gateway", "peers.tags": "Tags", diff --git a/tests/aurora_theme.test.js b/tests/aurora_theme.test.js index 3a88a761..9906ee1d 100644 --- a/tests/aurora_theme.test.js +++ b/tests/aurora_theme.test.js @@ -1377,3 +1377,60 @@ describe('aurora theme — dashboard UX fixes (ux-dash)', () => { assert.match(css, /\.res-gauge-info\b/, '.res-gauge-info rule in aurora.css'); }); }); + +// ── UX-fixes: Peers gateway card — badge inside, gear-edit, card→detail nav ── +describe('aurora theme — peers gateway card UX fixes (Issues 5/6/7)', () => { + it('auroraRenderGatewayCard builds badge inside the card using DOM (not detached)', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'peers.js'), 'utf8'); + // Badge is created with DOM createElement and appended inside uh (card header) + assert.match(js, /badge\.className\s*=\s*statusClass/, 'badge.className assigned from statusClass inside auroraRenderGatewayCard'); + assert.match(js, /right\.appendChild\(badge\)/, 'badge appended to the right-side header span (inside card)'); + // The "right" span is added to uh (header row), which is added to unit (card) + assert.match(js, /uh\.appendChild\(right\)/, 'right span appended to uh header row'); + assert.match(js, /unit\.appendChild\(uh\)/, 'uh header row appended to unit card'); + }); + + it('auroraRenderGatewayCard emits a gear button with data-action="edit" and data-id=peer_id', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'peers.js'), 'utf8'); + // Gear button gets setAttribute('data-action', 'edit') + assert.match(js, /gearBtn\.setAttribute\('data-action',\s*'edit'\)/, "gear button has data-action='edit'"); + assert.match(js, /gearBtn\.setAttribute\('data-id',\s*String\(gw\.peer_id\)\)/, 'gear button data-id is String(gw.peer_id)'); + // Gear button is appended inside the right span (inside card header) + assert.match(js, /right\.appendChild\(gearBtn\)/, 'gear button appended inside card header'); + }); + + it('auroraRenderGatewayCard gear button stops propagation and calls showEditModal', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'peers.js'), 'utf8'); + assert.match(js, /e\.stopPropagation\(\)[\s\S]{0,40}showEditModal\(gw\.peer_id\)/, 'gear click: stopPropagation then showEditModal(gw.peer_id)'); + }); + + it('auroraRenderGatewayCard sets dataset.gwDetail for test assertions and a11y', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'peers.js'), 'utf8'); + assert.match(js, /unit\.dataset\.gwDetail\s*=\s*'\/gateways#gw\/'/, "unit.dataset.gwDetail set to '/gateways#gw/' prefix"); + }); + + it('auroraRenderGatewayCard card click navigates to /gateways#gw/ (Issue 7)', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'peers.js'), 'utf8'); + assert.match(js, /window\.location\.href\s*=\s*'\/gateways#gw\/'/, "card click sets window.location.href to '/gateways#gw/' + peer_id"); + // Must NOT call showEditModal on card click (that's now the gear's job) + // Check: the card-click listener no longer contains showEditModal (the gear listener has it) + // We verify this by checking that the card-click handler only has window.location.href + const cardClickMatch = js.match(/unit\.addEventListener\('click',\s*function\(e\)\s*\{([\s\S]*?)\}\);/g); + assert.ok(cardClickMatch, 'unit addEventListener click handler present'); + const hasNav = cardClickMatch.some(function(s) { return /window\.location\.href/.test(s); }); + assert.ok(hasNav, 'card-click handler navigates via window.location.href'); + }); + + it('auroraRenderGatewayCard card click uses button/a guard (gear and badge excluded from nav)', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'peers.js'), 'utf8'); + // Card click guard: e.target.closest('button, a') prevents nav when gear is clicked + assert.match(js, /e\.target\.closest\('button,\s*a'\)[\s\S]{0,20}return/, 'card-click has button/a closest guard before nav'); + }); + + it('i18n has peers.gateway.action_edit_gear in both en.json and de.json', () => { + const en = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'src', 'i18n', 'en.json'), 'utf8')); + const de = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'src', 'i18n', 'de.json'), 'utf8')); + assert.ok(en['peers.gateway.action_edit_gear'], 'peers.gateway.action_edit_gear present in en.json'); + assert.ok(de['peers.gateway.action_edit_gear'], 'peers.gateway.action_edit_gear present in de.json'); + }); +}); From 9e3a20499f3a364328dc76365c97d79f69ea3420 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:26:53 +0200 Subject: [PATCH 3/7] =?UTF-8?q?fix(theme):=20Aurora=20gateways=20=E2=80=94?= =?UTF-8?q?=20badge=20inside+text-left,=20detail=20cards=201/3=20width=20+?= =?UTF-8?q?=20content=20fit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/css/aurora.css | 10 +++++++-- public/js/gateways.js | 42 ++++++++++++++++++++++++++++++++++---- tests/aurora_theme.test.js | 29 ++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/public/css/aurora.css b/public/css/aurora.css index a6360e36..f6f76dbe 100644 --- a/public/css/aurora.css +++ b/public/css/aurora.css @@ -579,6 +579,10 @@ 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(3,1fr);gap:14px} +.gw-detail-grid .gw.full{grid-column:1/-1} + /* ---------------------------------------------------------------------------- C · PEERS page — Aurora mockup fidelity (Task P2-3) --------------------------------------------------------------------------- */ @@ -586,8 +590,10 @@ input[type=date]:focus, select:focus { /* 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) diff --git a/public/js/gateways.js b/public/js/gateways.js index 23525425..94bff137 100644 --- a/public/js/gateways.js +++ b/public/js/gateways.js @@ -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') { @@ -97,13 +100,44 @@ 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)); diff --git a/tests/aurora_theme.test.js b/tests/aurora_theme.test.js index 9906ee1d..9dacb8d1 100644 --- a/tests/aurora_theme.test.js +++ b/tests/aurora_theme.test.js @@ -1434,3 +1434,32 @@ describe('aurora theme — peers gateway card UX fixes (Issues 5/6/7)', () => { assert.ok(de['peers.gateway.action_edit_gear'], 'peers.gateway.action_edit_gear present in de.json'); }); }); + +// ── UX-fixes: Gateways fleet card + detail (Issues 8/9/10/11) ──────────────── +describe('aurora theme — gateways UX fixes (Issues 8/9/10/11)', () => { + it('Issue 8: auroraCard builds badge inside card header using right container', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'gateways.js'), 'utf8'); + // stTag appended to right container, right container appended to uh (inside card) + assert.match(js, /right\.appendChild\(stTag\)/, 'badge (stTag) appended to right container'); + assert.match(js, /uh\.appendChild\(right\)/, 'right container appended to uh header row (inside card)'); + }); + + it('Issue 9: aurora.css has .tag.tag-dot::after (dot after text) and suppresses ::before', () => { + const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'aurora.css'), 'utf8'); + assert.match(css, /\.tag\.tag-dot::after/, '.tag.tag-dot::after present (dot positioned after text)'); + assert.match(css, /\.tag\.tag-dot::before\s*\{[^}]*content:\s*none/, '.tag.tag-dot::before has content:none (before-dot suppressed)'); + }); + + it('Issue 10: auroraVersionsCard() present and called from auroraRenderDetail()', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'gateways.js'), 'utf8'); + assert.match(js, /function auroraVersionsCard\(/, 'auroraVersionsCard() present in gateways.js'); + assert.match(js, /grid2\.appendChild\(auroraVersionsCard\(g\)\)/, 'auroraRenderDetail() calls auroraVersionsCard(g)'); + }); + + it('Issue 11: auroraRenderDetail uses gw-detail-grid and aurora.css has 3-column rule', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'gateways.js'), 'utf8'); + const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'aurora.css'), 'utf8'); + assert.match(js, /el\('div',\s*'gw-detail-grid'\)/, 'auroraRenderDetail() uses gw-detail-grid class'); + assert.match(css, /\.gw-detail-grid\s*\{[^}]*repeat\(3,1fr\)/, 'gw-detail-grid uses repeat(3,1fr) 3-column layout'); + }); +}); From 952ec5792d965fde09a74e5c91639c8ea43986f7 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:43:01 +0200 Subject: [PATCH 4/7] =?UTF-8?q?fix(theme):=20Aurora=20rdp=20=E2=80=94=20ed?= =?UTF-8?q?it-modal=20checkboxes,=20card=20consistency,=20status=20color/i?= =?UTF-8?q?con,=20badge=20inside?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/css/aurora.css | 2 + public/js/rdp.js | 80 ++++++++++++++++++++-------------- templates/aurora/pages/rdp.njk | 60 +++++++++++++++++++++++++ tests/aurora_theme.test.js | 66 ++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 32 deletions(-) diff --git a/public/css/aurora.css b/public/css/aurora.css index f6f76dbe..278b8791 100644 --- a/public/css/aurora.css +++ b/public/css/aurora.css @@ -730,6 +730,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) ─────────────────────── */ diff --git a/public/js/rdp.js b/public/js/rdp.js index 214bac12..15969c0a 100644 --- a/public/js/rdp.js +++ b/public/js/rdp.js @@ -558,8 +558,17 @@ if (checkBtn) { try { var result = await api.get('/api/v1/rdp/' + checkBtn.dataset.check + '/status'); - checkBtn.textContent = result.online ? 'Online' : 'Offline'; - checkBtn.style.color = result.online ? 'var(--success)' : 'var(--danger)'; + if (isAurora()) { + // Issue 16: Aurora — color + distinct icon, no big text in button + checkBtn.style.color = result.online ? 'var(--green)' : 'var(--red)'; + checkBtn.title = result.online ? (GC.t['rdp.online'] || 'Online') : (GC.t['rdp.offline'] || 'Offline'); + checkBtn.innerHTML = result.online + ? '' + : ''; + } else { + checkBtn.textContent = result.online ? 'Online' : 'Offline'; + checkBtn.style.color = result.online ? 'var(--success)' : 'var(--danger)'; + } } catch {} return; } @@ -1737,8 +1746,10 @@ return; } grid.style.cssText = ''; + // Issue 12: use unit-grid-style auto-fill grid so cards are consistently sized + // (not stretched span6 in 12-col grid like other Aurora pages) var container = document.createElement('div'); - container.className = 'grid'; + container.className = 'rdp-card-grid'; // Monitor SVG (matches mockup card-title icon) var MONITOR_SVG = ''; @@ -1747,10 +1758,11 @@ var isOnline = r.status && r.status.online; var isMaintenance = r.maintenance_enabled; + // Issue 12: plain .card (no span6), sized by the .rdp-card-grid container var card = document.createElement('div'); - card.className = 'card span6'; + card.className = 'card'; - // Card title: monitor icon + name + // Card title: monitor icon + name + proto badge + status badge (Issue 13: badge inside header) var cardTitle = document.createElement('div'); cardTitle.className = 'card-title'; var ic = document.createElement('span'); @@ -1762,13 +1774,26 @@ cardTitle.appendChild(nameSpan); // Proto badge alongside name var protoBadgeWrap = document.createElement('span'); - protoBadgeWrap.className = 'card-sub'; - protoBadgeWrap.style.cssText = 'margin-left:6px;display:inline-flex;align-items:center;gap:4px'; + protoBadgeWrap.style.cssText = 'margin-left:6px;display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:500;font-family:var(--font-body);color:var(--faint)'; protoBadgeWrap.appendChild(buildProtoBadge(r)); cardTitle.appendChild(protoBadgeWrap); + // Issue 13: status badge inside card header, text LEFT of dot (uses .tag.tag-dot::after) + var statusTag = document.createElement('span'); + if (isOnline) { + statusTag.className = 'tag tag-green tag-dot'; + statusTag.textContent = GC.t['rdp.health_reachable'] || 'Reachable'; + } else if (isMaintenance) { + statusTag.className = 'tag tag-amber tag-dot'; + statusTag.textContent = GC.t['rdp.health_checking'] || 'Checking…'; + } else { + statusTag.className = 'tag tag-red tag-dot'; + statusTag.textContent = GC.t['rdp.offline'] || 'Offline'; + } + statusTag.style.marginLeft = 'auto'; + cardTitle.appendChild(statusTag); card.appendChild(cardTitle); - // KV rows: Mode / Target / Health + // KV rows: Mode / Target (Health removed from kv — now in header as Issue 13) var kv = document.createElement('div'); kv.className = 'kv'; @@ -1799,30 +1824,6 @@ targetRow.appendChild(targetV); kv.appendChild(targetRow); - // Health row (tag with .tag-dot per mockup) - var healthRow = document.createElement('div'); - healthRow.className = 'row'; - var healthK = document.createElement('span'); - healthK.className = 'k'; - healthK.textContent = GC.t['rdp.kv.health'] || 'Health'; - var healthV = document.createElement('span'); - healthV.className = 'v'; - var healthTag = document.createElement('span'); - if (isOnline) { - healthTag.className = 'tag tag-green tag-dot'; - healthTag.textContent = GC.t['rdp.health_reachable'] || 'Reachable'; - } else if (isMaintenance) { - healthTag.className = 'tag tag-amber tag-dot'; - healthTag.textContent = GC.t['rdp.health_checking'] || 'Checking…'; - } else { - healthTag.className = 'tag tag-red tag-dot'; - healthTag.textContent = GC.t['rdp.offline'] || 'Offline'; - } - healthV.appendChild(healthTag); - healthRow.appendChild(healthK); - healthRow.appendChild(healthV); - kv.appendChild(healthRow); - card.appendChild(kv); // Action buttons — ALL actions from default card preserved, as .icon-action set @@ -1850,6 +1851,21 @@ rowActions.appendChild(disconnBtn); } + // Issue 14: browser session button — only when browser access is enabled and licensed. + // Real mechanism: GET /rdp/:id/session (confirmed in src/routes/index.js line 254). + if (r.browser_enabled && GC.features && GC.features.browser_sessions) { + var browserBtn = document.createElement('button'); + browserBtn.className = 'icon-action'; + browserBtn.title = GC.t['rdp.browser.open'] || 'Im Browser öffnen'; + (function (id) { + browserBtn.addEventListener('click', function () { + window.open('/rdp/' + id + '/session', '_blank', 'noopener'); + }); + }(r.id)); + browserBtn.innerHTML = ''; + rowActions.appendChild(browserBtn); + } + // Edit (always) var editBtn = document.createElement('button'); editBtn.className = 'icon-action'; diff --git a/templates/aurora/pages/rdp.njk b/templates/aurora/pages/rdp.njk index 52e3d262..d9bc392f 100644 --- a/templates/aurora/pages/rdp.njk +++ b/templates/aurora/pages/rdp.njk @@ -456,9 +456,69 @@
{# Browser-access section (hidden until toggle enabled; rdp.js reveals) #} + {# Issue 15: Add all 6 browser checkboxes + SFTP/audio inputs required by rdp.js populate code #} diff --git a/tests/aurora_theme.test.js b/tests/aurora_theme.test.js index 9dacb8d1..510fbfe7 100644 --- a/tests/aurora_theme.test.js +++ b/tests/aurora_theme.test.js @@ -1463,3 +1463,69 @@ describe('aurora theme — gateways UX fixes (Issues 8/9/10/11)', () => { assert.match(css, /\.gw-detail-grid\s*\{[^}]*repeat\(3,1fr\)/, 'gw-detail-grid uses repeat(3,1fr) 3-column layout'); }); }); + +// ── UX-fixes: RDP page (Issues 12/13/14/15/16) ─────────────────────────────── +describe('aurora theme — rdp UX fixes (Issues 12/13/14/15/16)', () => { + it('Issue 12: auroraRenderGrid uses rdp-card-grid container (not span6/full-width)', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'rdp.js'), 'utf8'); + const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'aurora.css'), 'utf8'); + // Container must use rdp-card-grid, not grid (which yields span6 half-width cards) + assert.match(js, /container\.className\s*=\s*'rdp-card-grid'/, "auroraRenderGrid uses 'rdp-card-grid' container"); + // Cards must not use span6 (which is half-width in 12-col grid) + assert.doesNotMatch(js, /card\.className\s*=\s*'card span6'/, "card.className no longer uses 'card span6'"); + // aurora.css must define the grid rule with auto-fill + assert.match(css, /\.rdp-card-grid\s*\{[^}]*auto-fill/, 'aurora.css .rdp-card-grid uses auto-fill grid'); + }); + + it('Issue 13: status badge built inside card header (cardTitle) with tag-dot (text-left-of-dot)', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'rdp.js'), 'utf8'); + // Status tag is appended to cardTitle (inside header), not to a separate health kv row + assert.match(js, /statusTag\.style\.marginLeft\s*=\s*'auto'/, 'statusTag has margin-left:auto (pushed to header right)'); + assert.match(js, /cardTitle\.appendChild\(statusTag\)/, 'statusTag appended to cardTitle (inside card header)'); + // Uses tag-dot class (text left of dot via ::after in aurora.css) + assert.match(js, /statusTag\.className\s*=\s*'tag tag-green tag-dot'/, 'online state uses tag-green tag-dot'); + assert.match(js, /statusTag\.className\s*=\s*'tag tag-red tag-dot'/, 'offline state uses tag-red tag-dot'); + }); + + it('Issue 14: browser session button wired to /rdp/:id/session (real mechanism)', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'rdp.js'), 'utf8'); + // Button only shown when browser_enabled + browser_sessions licensed + assert.match(js, /r\.browser_enabled && GC\.features && GC\.features\.browser_sessions/, 'browser button gated on browser_enabled+license'); + // Opens the real session URL + assert.match(js, /window\.open\('\/rdp\/' \+ id \+ '\/session'/, "browser button opens '/rdp/:id/session'"); + }); + + it('Issue 15: aurora rdp.njk has all 6 browser checkbox ids', () => { + const njk = fs.readFileSync(path.join(__dirname, '..', 'templates', 'aurora', 'pages', 'rdp.njk'), 'utf8'); + assert.match(njk, /id="rdp-browser-clipboard"/, 'rdp-browser-clipboard present in aurora rdp.njk'); + assert.match(njk, /id="rdp-browser-sftp"/, 'rdp-browser-sftp present in aurora rdp.njk'); + assert.match(njk, /id="rdp-sftp-disable-download"/, 'rdp-sftp-disable-download present in aurora rdp.njk'); + assert.match(njk, /id="rdp-sftp-disable-upload"/, 'rdp-sftp-disable-upload present in aurora rdp.njk'); + assert.match(njk, /id="rdp-browser-audio-rdp"/, 'rdp-browser-audio-rdp present in aurora rdp.njk'); + assert.match(njk, /id="rdp-browser-audio-vnc"/, 'rdp-browser-audio-vnc present in aurora rdp.njk'); + // Also check the SFTP text inputs needed by populate code (lines 1053-1062) + assert.match(njk, /id="rdp-sftp-host"/, 'rdp-sftp-host present (populate code sets .value)'); + assert.match(njk, /id="rdp-audio-servername"/, 'rdp-audio-servername present (populate code sets .value)'); + }); + + it('Issue 15: aurora rdp template renders (200) with all browser-section ids visible in HTML', async () => { + selectAurora(); + const res = await agent.get('/rdp').expect(200); + assert.match(res.text, /id="rdp-browser-clipboard"/, 'rdp-browser-clipboard in rendered HTML'); + assert.match(res.text, /id="rdp-browser-sftp"/, 'rdp-browser-sftp in rendered HTML'); + assert.match(res.text, /id="rdp-sftp-disable-download"/, 'rdp-sftp-disable-download in rendered HTML'); + assert.match(res.text, /id="rdp-sftp-disable-upload"/, 'rdp-sftp-disable-upload in rendered HTML'); + assert.match(res.text, /id="rdp-browser-audio-rdp"/, 'rdp-browser-audio-rdp in rendered HTML'); + assert.match(res.text, /id="rdp-browser-audio-vnc"/, 'rdp-browser-audio-vnc in rendered HTML'); + }); + + it('Issue 16: aurora check handler uses isAurora() branch — color+icon, not big text', () => { + const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'rdp.js'), 'utf8'); + // Must have isAurora() branch inside check handler + assert.match(js, /if \(isAurora\(\)\)[\s\S]{0,200}checkBtn\.style\.color/, 'isAurora() branch sets color on checkBtn'); + // Aurora branch sets innerHTML (icon), not textContent + assert.match(js, /checkBtn\.innerHTML\s*=\s*result\.online/, 'Aurora branch sets innerHTML to status icon on check result'); + // Non-aurora path still sets textContent + assert.match(js, /checkBtn\.textContent\s*=\s*result\.online/, 'non-aurora branch still sets textContent'); + }); +}); From fdd6f2fe1ad8c0c2ef89b4291c2e451fe757d265 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:51:58 +0200 Subject: [PATCH 5/7] fix(theme): Aurora settings aurora-theme option + card-height + sidebar no longer covers logo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 17: add data-default-theme="aurora" button to default-theme picker in aurora settings.njk (reuses profile.theme_aurora i18n key, API already accepts aurora). Issue 18: scope align-items:start to .settings-panel .grid in aurora.css so app-info card no longer stretches to match tall neighbours. Issue 19: add position:static to .sidebar rule in aurora.css so sidebar returns to grid flow and .app-brand logo+wordmark are no longer covered; mobile @media (max-width:980px) re-applies position:fixed for drawer — intact. 5 new tests in aurora_theme.test.js; 144/144 pass. --- public/css/aurora.css | 4 ++- templates/aurora/pages/settings.njk | 1 + tests/aurora_theme.test.js | 49 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/public/css/aurora.css b/public/css/aurora.css index 278b8791..9cd70c09 100644 --- a/public/css/aurora.css +++ b/public/css/aurora.css @@ -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; @@ -752,6 +752,8 @@ input[type=date]:focus, select:focus { /* Panel (moved from settings.njk