This commit is contained in:
1 parent
9a1efb2c15
commit
c7a0ed93ff
4 files changed
+88
-49
No files matched your search
@@ -299,7 +299,7 @@ async def get_ups_ui_tiles(ups_name: str, user=Depends(require_session)):
|
|||||||
raw = r.get(f"ups:ui:tiles:{ups_name}")
|
raw = r.get(f"ups:ui:tiles:{ups_name}")
|
||||||
default = {
|
default = {
|
||||||
"types": {}, "order": [], "hidden": [], "custom": [],
|
"types": {}, "order": [], "hidden": [], "custom": [],
|
||||||
"positions": {}, "card_size": None,
|
"positions": {}, "card_size": None, "exists": False,
|
||||||
}
|
}
|
||||||
if not raw:
|
if not raw:
|
||||||
return default
|
return default
|
||||||
@@ -312,6 +312,7 @@ async def get_ups_ui_tiles(ups_name: str, user=Depends(require_session)):
|
|||||||
"custom": data.get("custom", []),
|
"custom": data.get("custom", []),
|
||||||
"positions": data.get("positions", {}),
|
"positions": data.get("positions", {}),
|
||||||
"card_size": data.get("card_size"),
|
"card_size": data.get("card_size"),
|
||||||
|
"exists": True,
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
return default
|
return default
|
||||||
|
|||||||
+51
-47
@@ -234,10 +234,14 @@ async function loadServerTileConfig(name) {
|
|||||||
scheduleServerTileSave(name);
|
scheduleServerTileSave(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pendingSave = null;
|
const pendingSaves = {};
|
||||||
function scheduleServerTileSave(name) {
|
function scheduleServerTileSave(name) {
|
||||||
if (pendingSave) clearTimeout(pendingSave);
|
if (!name) return;
|
||||||
pendingSave = setTimeout(() => saveServerTileConfig(name), 800);
|
if (pendingSaves[name]) clearTimeout(pendingSaves[name]);
|
||||||
|
pendingSaves[name] = setTimeout(() => {
|
||||||
|
delete pendingSaves[name];
|
||||||
|
saveServerTileConfig(name);
|
||||||
|
}, 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoArrangeTiles(name, grid) {
|
function autoArrangeTiles(name, grid) {
|
||||||
@@ -314,14 +318,15 @@ function autoArrangeTiles(name, grid) {
|
|||||||
grid.style.height = `${cardHeight - 70}px`;
|
grid.style.height = `${cardHeight - 70}px`;
|
||||||
|
|
||||||
// Save card size
|
// Save card size
|
||||||
persistCardSize(name, cardWidth, cardHeight);
|
persistCardSize(name, cardWidth, cardHeight, false);
|
||||||
debugLog(`Auto-arranged ${tiles.length} tiles for ${name}, card size: ${cardWidth}x${cardHeight}`);
|
debugLog(`Auto-arranged ${tiles.length} tiles for ${name}, card size: ${cardWidth}x${cardHeight}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistCardSize(name, width, height) {
|
function persistCardSize(name, width, height, syncServer = true) {
|
||||||
savedCardSizes[name] = { width, height };
|
savedCardSizes[name] = { width, height };
|
||||||
saveToStorage(CARD_SIZE_KEY, savedCardSizes);
|
saveToStorage(CARD_SIZE_KEY, savedCardSizes);
|
||||||
|
if (syncServer) scheduleServerTileSave(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreCardSize(name) {
|
function restoreCardSize(name) {
|
||||||
@@ -340,6 +345,20 @@ function restoreCardSize(name) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasSavedPositions(name) {
|
||||||
|
return Object.keys(savedTilePos[name] || {}).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDefaultTileTypes(name) {
|
||||||
|
savedTileTypes[name] = savedTileTypes[name] || {
|
||||||
|
load_pct: 'gauge',
|
||||||
|
volt_line: 'value',
|
||||||
|
volt_output: 'value',
|
||||||
|
watts_usage: 'value'
|
||||||
|
};
|
||||||
|
try { localStorage.setItem(TILE_TYPE_KEY, JSON.stringify(savedTileTypes)); } catch{}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveServerTileConfig(name) {
|
async function saveServerTileConfig(name) {
|
||||||
const types = savedTileTypes[name] || {};
|
const types = savedTileTypes[name] || {};
|
||||||
const order = (savedTileOrder[name] || []);
|
const order = (savedTileOrder[name] || []);
|
||||||
@@ -407,7 +426,8 @@ function ensureUpsCard(name) {
|
|||||||
div.id = `card-${name}`;
|
div.id = `card-${name}`;
|
||||||
// Apply saved size if available
|
// Apply saved size if available
|
||||||
if (savedSizes[name]) {
|
if (savedSizes[name]) {
|
||||||
const { w, h } = savedSizes[name];
|
const w = savedSizes[name].width || savedSizes[name].w;
|
||||||
|
const h = savedSizes[name].height || savedSizes[name].h;
|
||||||
if (w) div.style.width = w + 'px';
|
if (w) div.style.width = w + 'px';
|
||||||
if (h) div.style.height = h + 'px';
|
if (h) div.style.height = h + 'px';
|
||||||
}
|
}
|
||||||
@@ -465,13 +485,7 @@ function ensureUpsCard(name) {
|
|||||||
}
|
}
|
||||||
// Set default tile types for new UPS (static layout)
|
// Set default tile types for new UPS (static layout)
|
||||||
if (!savedTileTypes[name]) {
|
if (!savedTileTypes[name]) {
|
||||||
savedTileTypes[name] = {
|
applyDefaultTileTypes(name);
|
||||||
load_pct: 'gauge',
|
|
||||||
volt_line: 'value',
|
|
||||||
volt_output: 'value',
|
|
||||||
watts_usage: 'value'
|
|
||||||
};
|
|
||||||
try { localStorage.setItem(TILE_TYPE_KEY, JSON.stringify(savedTileTypes)); } catch{}
|
|
||||||
}
|
}
|
||||||
// Initialize tiles
|
// Initialize tiles
|
||||||
initTilesFor(name, div.querySelector('[data-tile-grid]'));
|
initTilesFor(name, div.querySelector('[data-tile-grid]'));
|
||||||
@@ -677,6 +691,9 @@ function createCustomTile(name, grid, tileCfg) {
|
|||||||
tile.setAttribute('draggable','true');
|
tile.setAttribute('draggable','true');
|
||||||
const headerLabel = METRIC_LABELS[tileCfg.metric] ? METRIC_LABELS[tileCfg.metric] : tileCfg.metric;
|
const headerLabel = METRIC_LABELS[tileCfg.metric] ? METRIC_LABELS[tileCfg.metric] : tileCfg.metric;
|
||||||
tile.innerHTML = `<div class="tile-controls"><button data-remove-tile title="Remove">✕</button></div><h4>${escapeHtml(headerLabel)}</h4><div class="tile-body"></div>`;
|
tile.innerHTML = `<div class="tile-controls"><button data-remove-tile title="Remove">✕</button></div><h4>${escapeHtml(headerLabel)}</h4><div class="tile-body"></div>`;
|
||||||
|
const resizeHandle = document.createElement('div');
|
||||||
|
resizeHandle.className = 'tile-resize';
|
||||||
|
tile.appendChild(resizeHandle);
|
||||||
grid.appendChild(tile);
|
grid.appendChild(tile);
|
||||||
buildCustomTileVisualization(name, tile, tileCfg);
|
buildCustomTileVisualization(name, tile, tileCfg);
|
||||||
const removeBtn = tile.querySelector('[data-remove-tile]');
|
const removeBtn = tile.querySelector('[data-remove-tile]');
|
||||||
@@ -707,8 +724,9 @@ function rebuildAllTiles(name) {
|
|||||||
function removeCustomTile(name, id, tileEl) {
|
function removeCustomTile(name, id, tileEl) {
|
||||||
savedCustomTiles[name] = (savedCustomTiles[name]||[]).filter(t => t.id !== id);
|
savedCustomTiles[name] = (savedCustomTiles[name]||[]).filter(t => t.id !== id);
|
||||||
try { localStorage.setItem(CUSTOM_TILES_KEY, JSON.stringify(savedCustomTiles)); } catch(_) {}
|
try { localStorage.setItem(CUSTOM_TILES_KEY, JSON.stringify(savedCustomTiles)); } catch(_) {}
|
||||||
|
const grid = tileEl?.parentElement;
|
||||||
if (tileEl) tileEl.remove();
|
if (tileEl) tileEl.remove();
|
||||||
persistTileOrder(name, tileEl.parentElement);
|
if (grid) persistTileOrder(name, grid);
|
||||||
scheduleServerTileSave(name);
|
scheduleServerTileSave(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1508,22 +1526,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
savedTileTypes[name] = {
|
applyDefaultTileTypes(name);
|
||||||
load_pct: 'gauge',
|
|
||||||
volt_line: 'value',
|
|
||||||
volt_output: 'value',
|
|
||||||
watts_usage: 'value'
|
|
||||||
};
|
|
||||||
try { localStorage.setItem(TILE_TYPE_KEY, JSON.stringify(savedTileTypes)); } catch{}
|
|
||||||
|
|
||||||
const cfg = await loadServerTileConfig(name);
|
const cfg = await loadServerTileConfig(name);
|
||||||
if (cfg) {
|
if (cfg && cfg.exists !== false) {
|
||||||
savedTileTypes[name] = cfg.types || savedTileTypes[name] || {};
|
savedTileTypes[name] = Object.keys(cfg.types || {}).length ? cfg.types : savedTileTypes[name] || {};
|
||||||
savedTileOrder[name] = cfg.order || savedTileOrder[name] || [];
|
savedTileOrder[name] = (cfg.order || []).length ? cfg.order : savedTileOrder[name] || [];
|
||||||
const hiddenSet = {}; (cfg.hidden||[]).forEach(h => hiddenSet[h]=true);
|
const hiddenSet = {}; (cfg.hidden||[]).forEach(h => hiddenSet[h]=true);
|
||||||
savedTileHidden[name] = hiddenSet;
|
savedTileHidden[name] = hiddenSet;
|
||||||
savedCustomTiles[name] = cfg.custom || [];
|
savedCustomTiles[name] = (cfg.custom || []).length ? cfg.custom : savedCustomTiles[name] || [];
|
||||||
savedTilePos[name] = cfg.positions || savedTilePos[name] || {};
|
savedTilePos[name] = Object.keys(cfg.positions || {}).length ? cfg.positions : savedTilePos[name] || {};
|
||||||
if (cfg.card_size && cfg.card_size.width && cfg.card_size.height) {
|
if (cfg.card_size && cfg.card_size.width && cfg.card_size.height) {
|
||||||
savedCardSizes[name] = { width: cfg.card_size.width, height: cfg.card_size.height };
|
savedCardSizes[name] = { width: cfg.card_size.width, height: cfg.card_size.height };
|
||||||
saveToStorage(CARD_SIZE_KEY, savedCardSizes);
|
saveToStorage(CARD_SIZE_KEY, savedCardSizes);
|
||||||
@@ -1540,18 +1552,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Force rebuild tiles with correct types
|
// Force rebuild tiles with correct types
|
||||||
rebuildAllTiles(name);
|
rebuildAllTiles(name);
|
||||||
// If no stored positions existed, perform an auto layout
|
// If no stored positions existed, perform an auto layout
|
||||||
if (!cfg.positions || Object.keys(cfg.positions).length === 0) {
|
if (!hasSavedPositions(name)) {
|
||||||
resetLayout(name);
|
autoArrangeTiles(name, grid);
|
||||||
|
persistTilePositions(name, grid);
|
||||||
|
} else if (!Object.keys(cfg.positions || {}).length) {
|
||||||
|
scheduleServerTileSave(name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No server config: apply default static layout
|
// No server config: keep any local layout and backfill it to the server.
|
||||||
savedTileTypes[name] = {
|
applyDefaultTileTypes(name);
|
||||||
load_pct: 'gauge',
|
|
||||||
volt_line: 'value',
|
|
||||||
volt_output: 'value',
|
|
||||||
watts_usage: 'value'
|
|
||||||
};
|
|
||||||
try { localStorage.setItem(TILE_TYPE_KEY, JSON.stringify(savedTileTypes)); } catch{}
|
|
||||||
const grid = card.querySelector('[data-tile-grid]');
|
const grid = card.querySelector('[data-tile-grid]');
|
||||||
debugLog('Initializing default tiles for', name, 'grid found:', !!grid);
|
debugLog('Initializing default tiles for', name, 'grid found:', !!grid);
|
||||||
grid.innerHTML='';
|
grid.innerHTML='';
|
||||||
@@ -1559,18 +1568,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
debugLog('Tiles created, count:', grid.children.length);
|
debugLog('Tiles created, count:', grid.children.length);
|
||||||
// Force rebuild tiles with correct types
|
// Force rebuild tiles with correct types
|
||||||
rebuildAllTiles(name);
|
rebuildAllTiles(name);
|
||||||
// Apply static layout directly (same as in resetLayout)
|
if (!hasSavedPositions(name)) {
|
||||||
const loadTile = grid.querySelector('.tile[data-tile="load_pct"]');
|
autoArrangeTiles(name, grid);
|
||||||
const lineTile = grid.querySelector('.tile[data-tile="volt_line"]');
|
persistTilePositions(name, grid);
|
||||||
const outTile = grid.querySelector('.tile[data-tile="volt_output"]');
|
} else {
|
||||||
const wattsTile = grid.querySelector('.tile[data-tile="watts_usage"]');
|
scheduleServerTileSave(name);
|
||||||
if (loadTile) { loadTile.style.left='10px'; loadTile.style.top='10px'; loadTile.style.width='200px'; loadTile.style.height='160px'; }
|
}
|
||||||
if (lineTile) { lineTile.style.left='230px'; lineTile.style.top='10px'; lineTile.style.width='180px'; lineTile.style.height='70px'; }
|
|
||||||
if (outTile) { outTile.style.left='230px'; outTile.style.top='100px'; outTile.style.width='180px'; outTile.style.height='70px'; }
|
|
||||||
if (wattsTile) { wattsTile.style.left='10px'; wattsTile.style.top='190px'; wattsTile.style.width='400px'; wattsTile.style.height='200px'; }
|
|
||||||
setTimeout(()=>{ Object.values(charts).forEach(ch=>{ try{ ch.resize(); }catch{} }); }, 50);
|
setTimeout(()=>{ Object.values(charts).forEach(ch=>{ try{ ch.resize(); }catch{} }); }, 50);
|
||||||
persistTilePositions(name, grid);
|
|
||||||
scheduleServerTileSave(name);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -61,5 +61,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/static/js/dashboard.js?v=5"></script>
|
<script src="/static/js/dashboard.js?v=6"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -139,6 +139,40 @@ def test_ups_health_offline(authed_client, fake_redis):
|
|||||||
assert body["fail_count"] == 3
|
assert body["fail_count"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_tile_layout_preserves_tile_and_card_sizes(authed_client):
|
||||||
|
payload = {
|
||||||
|
"types": {"watts_usage": "line"},
|
||||||
|
"order": ["load_pct", "watts_usage"],
|
||||||
|
"hidden": [],
|
||||||
|
"custom": [],
|
||||||
|
"positions": {
|
||||||
|
"load_pct": {"left": 20, "top": 40, "width": 260, "height": 180},
|
||||||
|
"watts_usage": {"left": 320, "top": 40, "width": 520, "height": 260},
|
||||||
|
},
|
||||||
|
"card_size": {"width": 900, "height": 460},
|
||||||
|
}
|
||||||
|
|
||||||
|
r = authed_client.post("/api/ups/u1/ui_tiles", json=payload)
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
r = authed_client.get("/api/ups/u1/ui_tiles")
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.json()
|
||||||
|
assert body["exists"] is True
|
||||||
|
assert body["positions"]["load_pct"]["width"] == 260
|
||||||
|
assert body["positions"]["load_pct"]["height"] == 180
|
||||||
|
assert body["positions"]["watts_usage"]["width"] == 520
|
||||||
|
assert body["positions"]["watts_usage"]["height"] == 260
|
||||||
|
assert body["card_size"] == {"width": 900, "height": 460}
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_tile_layout_reports_missing_server_config(authed_client):
|
||||||
|
r = authed_client.get("/api/ups/u1/ui_tiles")
|
||||||
|
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["exists"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_battery_health_empty(authed_client):
|
def test_battery_health_empty(authed_client):
|
||||||
_add_ups(authed_client)
|
_add_ups(authed_client)
|
||||||
r = authed_client.get("/api/ups/u1/battery_health")
|
r = authed_client.get("/api/ups/u1/battery_health")
|
||||||
|
|||||||
Reference in new issue
Block a user