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 #}
{{ t('rdp.browser.section') }}
+ {# DA-2: SFTP + Audio are blocked for gateway/external access #}
+
{{ t('rdp.browser.gateway_blocked') }}
+ {# Clipboard (not gated by access mode) #}
+
+ {# SFTP toggle #}
+
+
+
+ {# Audio — rdp (rdp_disable_audio shown INVERTED as "Audio active") #}
+
+
+
+ {# Audio — vnc (browser_enable_audio) #}
+
+
+
+ {# Advanced — SFTP locks + secondary host/creds + audio servername #}
+
+ {{ t('rdp.browser.advanced') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('rdp.browser.set_kept') }}
+
+
+
+
+
{{ t('rdp.browser.set_kept') }}
+
+
+
+
+
{{ t('rdp.browser.set_kept') }}
+
+
+
+
+
+
+
+
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