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 ; }
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 "> ☾</ 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 ? '☼' : '☾' ;
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