feat(ui,isotest): Group disabled displays into a collapsible section and refactor display item rendering; start a background screenshot loop during isotest and improve test-run cleanup

This commit is contained in:
2026-01-10 20:40:27 +00:00
parent f85241dcd5
commit aedcc3f875
3 changed files with 54 additions and 19 deletions

View File

@@ -1,5 +1,13 @@
# Changelog # Changelog
## 2026-01-10 - 0.5.0 - feat(ui,isotest)
Group disabled displays into a collapsible section and refactor display item rendering; start a background screenshot loop during isotest and improve test-run cleanup
- Refactored display rendering: introduced renderDisplayItem() and simplified updateDisplaysUI() to separate enabled/disabled displays
- Disabled displays are collapsed under a <details> summary showing count ("Disabled Displays (N)")
- Added a background screenshot loop in isotest/run-test.sh that runs screenshot.sh every 5 seconds and records SCREENSHOT_LOOP_PID
- Improved cleanup in isotest/run-test.sh to kill SCREENSHOT_LOOP_PID and ENABLE_PID if they are running
## 2026-01-10 - 0.4.15 - fix(isotest) ## 2026-01-10 - 0.4.15 - fix(isotest)
Improve robustness of SPICE display enabler: add logging, wait-for-port and URI parsing, retries and reconnection logic, stabilization delay before configuring, and verification/retry of monitor configuration Improve robustness of SPICE display enabler: add logging, wait-for-port and URI parsing, retries and reconnection logic, stabilization delay before configuring, and verification/retry of monitor configuration

View File

@@ -725,31 +725,48 @@ export class UIServer {
setInterval(fetchUpdates, 60000); // Check every minute setInterval(fetchUpdates, 60000); // Check every minute
// Display management // Display management
function renderDisplayItem(d) {
return '<div class="device-item" style="flex-wrap: wrap; gap: 8px;">' +
'<div style="flex: 1; min-width: 150px;">' +
'<div class="device-name">' + d.name + '</div>' +
'<div style="font-size: 11px; color: var(--text-dim);">' +
d.width + 'x' + d.height + ' @ ' + d.refreshRate + 'Hz' +
(d.make !== 'Unknown' ? ' • ' + d.make : '') +
'</div>' +
'</div>' +
'<div style="display: flex; gap: 4px;">' +
(d.isPrimary
? '<span class="device-default">Primary</span>'
: (d.active ? '<button class="btn btn-primary" style="padding: 2px 8px; margin: 0; font-size: 11px;" onclick="setKioskDisplay(\\'' + d.name + '\\')">Set Primary</button>' : '')) +
'<button class="btn ' + (d.active ? 'btn-danger' : 'btn-primary') + '" style="padding: 2px 8px; margin: 0; font-size: 11px;" onclick="toggleDisplay(\\'' + d.name + '\\', ' + !d.active + ')">' +
(d.active ? 'Disable' : 'Enable') +
'</button>' +
'</div>' +
'</div>';
}
function updateDisplaysUI(data) { function updateDisplaysUI(data) {
const list = document.getElementById('displays-list'); const list = document.getElementById('displays-list');
if (!data.displays || data.displays.length === 0) { if (!data.displays || data.displays.length === 0) {
list.innerHTML = '<div style="color: var(--text-dim);">No displays detected</div>'; list.innerHTML = '<div style="color: var(--text-dim);">No displays detected</div>';
return; return;
} }
list.innerHTML = data.displays.map(d =>
'<div class="device-item" style="flex-wrap: wrap; gap: 8px;">' + const enabled = data.displays.filter(d => d.active);
'<div style="flex: 1; min-width: 150px;">' + const disabled = data.displays.filter(d => !d.active);
'<div class="device-name">' + d.name + '</div>' +
'<div style="font-size: 11px; color: var(--text-dim);">' + let html = enabled.map(renderDisplayItem).join('');
d.width + 'x' + d.height + ' @ ' + d.refreshRate + 'Hz' +
(d.make !== 'Unknown' ? ' • ' + d.make : '') + if (disabled.length > 0) {
'</div>' + html += '<details style="margin-top: 12px;">' +
'<summary style="cursor: pointer; color: var(--text-dim); font-size: 12px; padding: 4px 0;">Disabled Displays (' + disabled.length + ')</summary>' +
'<div style="margin-top: 8px;">' +
disabled.map(renderDisplayItem).join('') +
'</div>' + '</div>' +
'<div style="display: flex; gap: 4px;">' + '</details>';
(d.isPrimary }
? '<span class="device-default">Primary</span>'
: '<button class="btn btn-primary" style="padding: 2px 8px; margin: 0; font-size: 11px;" onclick="setKioskDisplay(\\'' + d.name + '\\')">Set Primary</button>') + list.innerHTML = html;
'<button class="btn ' + (d.active ? 'btn-danger' : 'btn-primary') + '" style="padding: 2px 8px; margin: 0; font-size: 11px;" onclick="toggleDisplay(\\'' + d.name + '\\', ' + !d.active + ')">' +
(d.active ? 'Disable' : 'Enable') +
'</button>' +
'</div>' +
'</div>'
).join('');
} }
function fetchDisplays() { function fetchDisplays() {

View File

@@ -63,6 +63,12 @@ fi
cleanup() { cleanup() {
echo "" echo ""
echo "Shutting down..." echo "Shutting down..."
if [ -n "$SCREENSHOT_LOOP_PID" ] && kill -0 "$SCREENSHOT_LOOP_PID" 2>/dev/null; then
kill "$SCREENSHOT_LOOP_PID" 2>/dev/null || true
fi
if [ -n "$ENABLE_PID" ] && kill -0 "$ENABLE_PID" 2>/dev/null; then
kill "$ENABLE_PID" 2>/dev/null || true
fi
if [ -n "$VIEWER_PID" ] && kill -0 "$VIEWER_PID" 2>/dev/null; then if [ -n "$VIEWER_PID" ] && kill -0 "$VIEWER_PID" 2>/dev/null; then
kill "$VIEWER_PID" 2>/dev/null || true kill "$VIEWER_PID" 2>/dev/null || true
fi fi
@@ -212,11 +218,15 @@ if [ -f "$SCRIPT_DIR/enable-displays.py" ]; then
fi fi
echo "Tips:" echo "Tips:"
echo " - pnpm run test:screenshot - Take screenshot"
echo " - http://localhost:3006 - Management UI" echo " - http://localhost:3006 - Management UI"
echo " - socat - UNIX-CONNECT:.nogit/vm/serial.sock - Serial console (login: ecouser/ecouser)" echo " - socat - UNIX-CONNECT:.nogit/vm/serial.sock - Serial console (login: ecouser/ecouser)"
echo "" echo ""
# Start screenshot loop in background (takes screenshots every 5 seconds)
echo "Starting screenshot loop..."
(while true; do "$SCRIPT_DIR/screenshot.sh" 2>/dev/null; sleep 5; done) &
SCREENSHOT_LOOP_PID=$!
if [ "$AUTO_MODE" = true ]; then if [ "$AUTO_MODE" = true ]; then
echo "=== Auto mode: waiting for display setup ===" echo "=== Auto mode: waiting for display setup ==="