359359 < a class ="sidebar-link " data-view ="month ">
360360 < span class ="icon "> 📅</ span > {{ t.month_trend }}
361361 </ a >
362+ {% if bqm_configured %}
363+ < a class ="sidebar-link " data-view ="bqm ">
364+ < span class ="icon "> 📈</ span > {{ t.bqm_title }}
365+ </ a >
366+ {% endif %}
362367 < div class ="sidebar-divider "> </ div >
363368 < a class ="sidebar-link " href ="/settings ">
364369 < span class ="icon "> ⚙</ span > {{ t.settings }}
@@ -605,6 +610,21 @@ <h2 class="section-title">{{ t.upstream }} ({{ us|length }} {{ t.channels }})</h
605610 </ div >
606611 </ div >
607612</ div >
613+
614+ <!-- ═══ View: BQM Graphs ═══ -->
615+ {% if bqm_configured %}
616+ < div id ="view-bqm " class ="view ">
617+ < div class ="trend-header ">
618+ < span class ="trend-title "> {{ t.bqm_title }}</ span >
619+ </ div >
620+ < div id ="bqm-content ">
621+ < div id ="bqm-image-wrap " style ="text-align:center; ">
622+ < img id ="bqm-image " style ="max-width:100%; border-radius:8px; display:none; " alt ="BQM Graph ">
623+ </ div >
624+ < div id ="bqm-no-data " class ="no-data-msg " style ="display:none; "> </ div >
625+ </ div >
626+ </ div >
627+ {% endif %}
608628</ div > <!-- /main-content -->
609629</ div > <!-- /app-main -->
610630</ div > <!-- /app-layout -->
@@ -719,15 +739,21 @@ <h2>{{ t.export_title }}</h2>
719739 } ) ;
720740 var dashView = document . getElementById ( 'view-dashboard' ) ;
721741 var trendView = document . getElementById ( 'view-trends' ) ;
742+ var bqmView = document . getElementById ( 'view-bqm' ) ;
743+ dashView . classList . remove ( 'active' ) ;
744+ trendView . classList . remove ( 'active' ) ;
745+ if ( bqmView ) bqmView . classList . remove ( 'active' ) ;
746+ stopAutoRefresh ( ) ;
722747 if ( view === 'live' ) {
723748 dashView . classList . add ( 'active' ) ;
724- trendView . classList . remove ( 'active' ) ;
725749 updateDateLabel ( ) ;
726750 if ( ! isHistorical ) startAutoRefresh ( ) ;
751+ } else if ( view === 'bqm' ) {
752+ if ( bqmView ) bqmView . classList . add ( 'active' ) ;
753+ updateDateLabel ( ) ;
754+ loadBqmGraph ( selectedDate ) ;
727755 } else {
728- dashView . classList . remove ( 'active' ) ;
729756 trendView . classList . add ( 'active' ) ;
730- stopAutoRefresh ( ) ;
731757 updateDateLabel ( ) ;
732758 loadTrends ( view , selectedDate ) ;
733759 }
@@ -741,6 +767,8 @@ <h2>{{ t.export_title }}</h2>
741767 function updateDateLabel ( ) {
742768 if ( currentView === 'live' ) {
743769 dateLabel . textContent = isHistorical ? formatDateDE ( '{{ snapshot_ts[:10] if snapshot_ts else "" }}' ) : T . live ;
770+ } else if ( currentView === 'bqm' ) {
771+ dateLabel . textContent = formatDateDE ( selectedDate ) + ' (BQM)' ;
744772 } else {
745773 var rangeLabel = { day : T . day , week : T . week , month : T . month } [ currentView ] || '' ;
746774 dateLabel . textContent = formatDateDE ( selectedDate ) + ' (' + rangeLabel + ')' ;
@@ -752,7 +780,7 @@ <h2>{{ t.export_title }}</h2>
752780
753781 function dateNav ( dir ) {
754782 var d = new Date ( selectedDate + 'T12:00:00' ) ;
755- if ( currentView === 'live' || currentView === 'day' ) {
783+ if ( currentView === 'live' || currentView === 'day' || currentView === 'bqm' ) {
756784 d . setDate ( d . getDate ( ) + dir ) ;
757785 } else if ( currentView === 'week' ) {
758786 d . setDate ( d . getDate ( ) + dir * 7 ) ;
@@ -769,6 +797,8 @@ <h2>{{ t.export_title }}</h2>
769797 window . location . href = '/?t=' + encodeURIComponent ( snap . timestamp ) ;
770798 }
771799 } ) ;
800+ } else if ( currentView === 'bqm' ) {
801+ loadBqmGraph ( selectedDate ) ;
772802 } else {
773803 loadTrends ( currentView , selectedDate ) ;
774804 }
@@ -800,6 +830,8 @@ <h2>{{ t.export_title }}</h2>
800830 renderCalendar ( ) ;
801831 } ) ;
802832
833+ var bqmDates = [ ] ;
834+
803835 function loadCalendarData ( ) {
804836 fetch ( '/api/calendar' )
805837 . then ( function ( r ) { return r . json ( ) ; } )
@@ -808,6 +840,10 @@ <h2>{{ t.export_title }}</h2>
808840 calendarLoaded = true ;
809841 renderCalendar ( ) ;
810842 } ) ;
843+ fetch ( '/api/bqm/dates' )
844+ . then ( function ( r ) { return r . json ( ) ; } )
845+ . then ( function ( dates ) { bqmDates = dates || [ ] ; } )
846+ . catch ( function ( ) { } ) ;
811847 }
812848
813849 function renderCalendar ( ) {
@@ -830,7 +866,8 @@ <h2>{{ t.export_title }}</h2>
830866 var el = document . createElement ( 'span' ) ;
831867 el . className = 'cal-day' ;
832868 el . textContent = d ;
833- if ( datesWithData . indexOf ( dateStr ) !== - 1 ) el . classList . add ( 'has-data' ) ;
869+ var activeDates = ( currentView === 'bqm' ) ? bqmDates : datesWithData ;
870+ if ( activeDates . indexOf ( dateStr ) !== - 1 ) el . classList . add ( 'has-data' ) ;
834871 if ( dateStr === today ) el . classList . add ( 'today' ) ;
835872 if ( dateStr === selectedDate ) el . classList . add ( 'selected' ) ;
836873 el . setAttribute ( 'data-date' , dateStr ) ;
@@ -847,6 +884,9 @@ <h2>{{ t.export_title }}</h2>
847884 window . location . href = '/?t=' + encodeURIComponent ( snap . timestamp ) ;
848885 }
849886 } ) ;
887+ } else if ( currentView === 'bqm' ) {
888+ updateDateLabel ( ) ;
889+ loadBqmGraph ( selectedDate ) ;
850890 } else {
851891 updateDateLabel ( ) ;
852892 loadTrends ( currentView , selectedDate ) ;
@@ -947,6 +987,22 @@ <h2>{{ t.export_title }}</h2>
947987 } ) ;
948988 }
949989
990+ /* ── BQM Graph ── */
991+ function loadBqmGraph ( date ) {
992+ var img = document . getElementById ( 'bqm-image' ) ;
993+ var noData = document . getElementById ( 'bqm-no-data' ) ;
994+ if ( ! img || ! noData ) return ;
995+ img . style . display = 'none' ;
996+ noData . style . display = 'none' ;
997+ img . onload = function ( ) { img . style . display = 'inline-block' ; } ;
998+ img . onerror = function ( ) {
999+ img . style . display = 'none' ;
1000+ noData . textContent = T . bqm_no_data || 'No BQM graph for this date.' ;
1001+ noData . style . display = 'block' ;
1002+ } ;
1003+ img . src = '/api/bqm/image/' + date ;
1004+ }
1005+
9501006 /* ── Init ── */
9511007 updateDateLabel ( ) ;
9521008} ) ( ) ;
0 commit comments