Skip to content

Commit 08014e1

Browse files
itsDNNSclaude
andcommitted
Add AJAX live refresh and dark/light theme toggle
Replace full page reload with AJAX fetch + selective DOM update to preserve UI state (open modals, expanded cards, channel groups). Add theme toggle button in topbar corner. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 22cc9a9 commit 08014e1

File tree

1 file changed

+86
-7
lines changed

1 file changed

+86
-7
lines changed

app/templates/index.html

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@
7373
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
7474
}
7575
.topbar-meta { font-size: 0.8em; color: var(--muted); white-space: nowrap; }
76+
.theme-toggle {
77+
background: none; border: 1px solid var(--input-border);
78+
color: var(--muted); font-size: 1.1em; cursor: pointer;
79+
padding: 4px 8px; border-radius: 4px; line-height: 1;
80+
}
81+
.theme-toggle:hover { color: var(--accent); border-color: var(--accent); }
7682

7783
/* ── Layout ── */
7884
.app-layout { display: flex; min-height: 100vh; }
@@ -526,6 +532,7 @@
526532
<span class="topbar-meta" id="topbar-meta">
527533
{% if last_update %}{{ t.last_update }}: {{ last_update }} | {{ poll_interval }}s{% endif %}
528534
</span>
535+
<button class="theme-toggle" id="theme-toggle" title="Toggle theme">&#9790;</button>
529536
</div>
530537

531538
<!-- Calendar Popup -->
@@ -894,6 +901,20 @@ <h2>{{ t.export_title }}</h2>
894901
var saved = localStorage.getItem('docsis-theme');
895902
if (saved) document.documentElement.setAttribute('data-theme', saved);
896903

904+
var themeBtn = document.getElementById('theme-toggle');
905+
function updateThemeIcon() {
906+
var isDark = document.documentElement.getAttribute('data-theme') !== 'light';
907+
themeBtn.innerHTML = isDark ? '&#9788;' : '&#9790;';
908+
}
909+
updateThemeIcon();
910+
themeBtn.addEventListener('click', function() {
911+
var isDark = document.documentElement.getAttribute('data-theme') !== 'light';
912+
var next = isDark ? 'light' : 'dark';
913+
document.documentElement.setAttribute('data-theme', next);
914+
localStorage.setItem('docsis-theme', next);
915+
updateThemeIcon();
916+
});
917+
897918
/* ── State ── */
898919
var currentView = 'live';
899920
var selectedDate = todayStr();
@@ -914,14 +935,17 @@ <h2>{{ t.export_title }}</h2>
914935
}
915936

916937
/* ── Sortable Tables ── */
917-
document.querySelectorAll('table.sortable').forEach(function(table) {
918-
var headers = table.querySelectorAll('th');
919-
headers.forEach(function(th, colIdx) {
920-
th.addEventListener('click', function() {
921-
sortTable(table, colIdx, th);
938+
function initSortableTables() {
939+
document.querySelectorAll('table.sortable').forEach(function(table) {
940+
var headers = table.querySelectorAll('th');
941+
headers.forEach(function(th, colIdx) {
942+
th.addEventListener('click', function() {
943+
sortTable(table, colIdx, th);
944+
});
922945
});
923946
});
924-
});
947+
}
948+
initSortableTables();
925949

926950
function sortTable(table, colIdx, th) {
927951
var tbody = table.querySelector('tbody');
@@ -1138,13 +1162,68 @@ <h2>{{ t.export_title }}</h2>
11381162
stopAutoRefresh();
11391163
refreshTimer = setInterval(function() {
11401164
if (currentView === 'live' && !isHistorical && !document.hidden) {
1141-
location.reload();
1165+
refreshData();
11421166
}
11431167
}, 60000);
11441168
}
11451169
function stopAutoRefresh() {
11461170
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
11471171
}
1172+
1173+
function refreshData() {
1174+
fetch(window.location.href)
1175+
.then(function(r) { return r.text(); })
1176+
.then(function(html) {
1177+
var doc = new DOMParser().parseFromString(html, 'text/html');
1178+
1179+
// Save expanded metric cards by index
1180+
var openCardIndices = [];
1181+
document.querySelectorAll('.metric-card').forEach(function(el, i) {
1182+
if (el.classList.contains('open')) openCardIndices.push(i);
1183+
});
1184+
1185+
// Save expanded channel groups by label text
1186+
var openGroupLabels = [];
1187+
document.querySelectorAll('details.channel-group[open]').forEach(function(el) {
1188+
var s = el.querySelector('summary');
1189+
if (s) openGroupLabels.push(s.textContent.trim());
1190+
});
1191+
1192+
// Replace dashboard content
1193+
var freshDash = doc.querySelector('#view-dashboard');
1194+
var currentDash = document.querySelector('#view-dashboard');
1195+
if (freshDash && currentDash) {
1196+
currentDash.innerHTML = freshDash.innerHTML;
1197+
}
1198+
1199+
// Update topbar timestamp
1200+
var freshMeta = doc.querySelector('#topbar-meta');
1201+
var currentMeta = document.querySelector('#topbar-meta');
1202+
if (freshMeta && currentMeta) {
1203+
currentMeta.innerHTML = freshMeta.innerHTML;
1204+
}
1205+
1206+
// Restore expanded metric cards
1207+
document.querySelectorAll('.metric-card').forEach(function(el, i) {
1208+
if (openCardIndices.indexOf(i) !== -1) el.classList.add('open');
1209+
});
1210+
1211+
// Restore expanded channel groups
1212+
document.querySelectorAll('details.channel-group').forEach(function(el) {
1213+
var s = el.querySelector('summary');
1214+
if (s && openGroupLabels.indexOf(s.textContent.trim()) !== -1) {
1215+
el.setAttribute('open', '');
1216+
}
1217+
});
1218+
1219+
// Re-init sortable tables (event listeners lost on innerHTML replace)
1220+
initSortableTables();
1221+
})
1222+
.catch(function(err) {
1223+
console.warn('Auto-refresh failed:', err);
1224+
});
1225+
}
1226+
11481227
if (!isHistorical) startAutoRefresh();
11491228

11501229
/* ── Trend Charts ── */

0 commit comments

Comments
 (0)