diff --git a/app/static/css/style.css b/app/static/css/style.css index cd3a36a..aeb9d72 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -24,6 +24,7 @@ } .card:hover { border-color: var(--border-hi); box-shadow: var(--shadow-md); } .card.resizing { outline: 2px dashed var(--accent); } +#ups-cards.resize-disabled .card { resize: none; } .card-header { display: flex; diff --git a/app/static/js/dashboard.js b/app/static/js/dashboard.js index 4714e6e..945152e 100644 --- a/app/static/js/dashboard.js +++ b/app/static/js/dashboard.js @@ -136,6 +136,7 @@ try { } catch(_) {} // Set defaults uiCfg.allow_resize = uiCfg.allow_resize !== false; +if (uiCfgEl) uiCfgEl.classList.toggle('resize-disabled', uiCfg.allow_resize === false); // Human-friendly metric labels for dropdown & headers const METRIC_LABELS = { 'LOADPCT': 'UPS Load %', @@ -155,8 +156,12 @@ const lastUpdateTs = {}; const UPS_STALE_SECONDS = 120; // mark offline after 2 minutes inactivity // Freeform tile layout enhancements const SNAP_GRID = 20; // pixel grid for snapping +const SNAP_ORIGIN = 10; // default tile layout starts at 10px let selectedTiles = new Set(); -function snap(v) { return Math.round(v / SNAP_GRID) * SNAP_GRID; } +function snap(v) { + if (v <= SNAP_ORIGIN) return SNAP_ORIGIN; + return SNAP_ORIGIN + Math.round((v - SNAP_ORIGIN) / SNAP_GRID) * SNAP_GRID; +} function clearSelection() { selectedTiles.forEach(t => t.classList.remove('selected')); selectedTiles.clear(); } function toggleTileSelection(tile, additive) { if (!additive) clearSelection(); @@ -164,6 +169,20 @@ function toggleTileSelection(tile, additive) { else { tile.classList.add('selected'); selectedTiles.add(tile); } } +function applyCardFeatureVisibility(card) { + const runtimeBadge = card.querySelector('[data-badge="runtime"]'); + const wattsBadge = card.querySelector('[data-badge="watts"]'); + const headroomBadge = card.querySelector('[data-badge="headroom"]'); + if (runtimeBadge) runtimeBadge.hidden = uiCfg.show_runtime === false; + if (wattsBadge) wattsBadge.hidden = uiCfg.show_watts === false; + if (headroomBadge) headroomBadge.hidden = uiCfg.show_headroom === false; +} + +function clearEventFooter(name) { + const footer = getCard(name)?.querySelector('[data-ups-footer]'); + if (footer) footer.textContent = ''; +} + // --- Redis-backed tile layout persistence --- async function loadServerTileConfig(name) { try { @@ -418,6 +437,7 @@ function ensureUpsCard(name) { if (!card.style.width && savedCardSizes[name]) { restoreCardSize(name); } + applyCardFeatureVisibility(card); return card; } const container = document.getElementById('ups-cards'); @@ -459,6 +479,7 @@ function ensureUpsCard(name) { attachResizeBehavior(div, handle); } container.appendChild(div); + applyCardFeatureVisibility(div); // Observe card for resize events to update footer position if (typeof ro !== 'undefined') { @@ -1029,7 +1050,11 @@ evtSource.onmessage = (e) => { const conn = card.querySelector('.ups-connection'); if (conn) conn.textContent = `${meta.host}:${meta.port}`; // Update event footer asynchronously (lightweight) - updateEventFooter(meta.name); + if (uiCfg.show_events === false) { + clearEventFooter(meta.name); + } else { + updateEventFooter(meta.name); + } }); Object.entries(snapshots).forEach(([name, snap]) => { diff --git a/app/static/js/settings.js b/app/static/js/settings.js index 9faae24..42238a5 100644 --- a/app/static/js/settings.js +++ b/app/static/js/settings.js @@ -42,6 +42,10 @@ show_events: f.show_events.checked, show_energy: f.show_energy.checked, color_badges: f.color_badges.checked, + show_runtime: f.show_runtime.checked, + show_watts: f.show_watts.checked, + show_headroom: f.show_headroom.checked, + allow_resize: f.allow_resize.checked, enable_transfer_burst_alert: f.enable_transfer_burst_alert.checked, enable_voltage_deviation_alert: f.enable_voltage_deviation_alert.checked, energy_cost_per_kwh: Number.parseFloat(normalizeEnergyRate(f.energy_cost_per_kwh.value)), diff --git a/app/templates/settings.html b/app/templates/settings.html index 182a37b..c669dd2 100644 --- a/app/templates/settings.html +++ b/app/templates/settings.html @@ -30,6 +30,34 @@ +
+
+ Show runtime badge + Display estimated runtime on each UPS card +
+ +
+
+
+ Show watts badge + Display current load in watts on each UPS card +
+ +
+
+
+ Show headroom badge + Display remaining load capacity on each UPS card +
+ +
+
+
+ Allow resizing + Enable card and tile resize handles on the dashboard +
+ +
Transfer-burst alerts diff --git a/tests/test_config_api.py b/tests/test_config_api.py index 524ac94..cbceb24 100644 --- a/tests/test_config_api.py +++ b/tests/test_config_api.py @@ -127,12 +127,23 @@ def test_ui_config_default(authed_client): def test_ui_config_update(authed_client): r = authed_client.put( "/api/config/ui", - json={"show_energy": True, "energy_cost_per_kwh": 0.15}, + json={ + "show_energy": True, + "energy_cost_per_kwh": 0.15, + "show_runtime": False, + "show_watts": False, + "show_headroom": False, + "allow_resize": False, + }, ) assert r.status_code == 200 body = r.json() assert body["ui"]["show_energy"] is True assert body["ui"]["energy_cost_per_kwh"] == 0.15 + assert body["ui"]["show_runtime"] is False + assert body["ui"]["show_watts"] is False + assert body["ui"]["show_headroom"] is False + assert body["ui"]["allow_resize"] is False def test_ui_config_energy_cost_keeps_four_decimal_rate(authed_client):