@@ -10,10 +10,21 @@
< script src = "/js/theme.js" > < / script >
< style >
/* Dashboard-spezifische Widgets (rechte Spalte) */
. gauge { display : flex ; flex-direction : column ; align-items : center ; gap : .2 rem ; }
. gauge svg { width : 130 px ; height : 130 px ; }
. gauge . ok { color : var ( - - accent ) ; font-weight : 800 ; font-size : 1 rem ; letter-spacing : .05 em ; margin-top : .3 rem ; text-shadow : 0 0 12 px var ( - - glow - s ) ; }
. gauge . op { color : var ( - - muted ) ; font-size : .72 rem ; }
. network-status { display : grid ; grid-template-columns : auto 1 fr ; gap : .85 rem ; align-items : start ; }
. network-status svg { width : 54 px ; height : 54 px ; color : var ( - - accent ) ; filter : drop-shadow ( 0 0 8 px var ( - - glow - s ) ) ; }
. status-summary { color : var ( - - heading ) ; font-size : .9 rem ; font-weight : 800 ; letter-spacing : .04 em ; }
. status-summary :: before { content : "" ; display : inline-block ; width : .52 rem ; height : .52 rem ; margin-right : .42 rem ; border-radius : 50 % ; background : var ( - - muted ) ; box-shadow : 0 0 8 px color-mix ( in srgb , var ( - - muted ) 60 % , transparent ) ; }
. network-status . ready . status-summary { color : var ( - - accent ) ; }
. network-status . ready . status-summary :: before { background : var ( - - accent ) ; box-shadow : 0 0 8 px var ( - - glow - s ) ; }
. network-status . restricted . status-summary { color : #edcb4c ; }
. network-status . restricted . status-summary :: before { background : #edcb4c ; box-shadow : 0 0 8 px rgba ( 237 , 203 , 76 , .48 ) ; }
. status-caption { color : var ( - - muted ) ; font-size : .72 rem ; line-height : 1.45 ; margin : .25 rem 0 0 ; }
. status-details { grid-column : 1 / -1 ; display : none ; gap : .45 rem ; padding-top : .2 rem ; color : var ( - - muted ) ; font-size : .72 rem ; }
. status-details . open { display : grid ; }
. status-details div { display : flex ; justify-content : space-between ; gap : 1 rem ; border-top : 1 px solid var ( - - border ) ; padding-top : .45 rem ; }
. status-details b { color : var ( - - text ) ; font-weight : 600 ; text-align : right ; }
. status-toggle { grid-column : 1 / -1 ; justify-self : start ; border : 0 ; padding : 0 ; background : transparent ; color : var ( - - secondary ) ; font : inherit ; font-size : .72 rem ; cursor : pointer ; }
. status-toggle : hover { text-decoration : underline ; }
. prog-top { display : flex ; justify-content : space-between ; align-items : center ; margin-bottom : .4 rem ; }
. pill { font-size : .68 rem ; color : var ( - - muted ) ; border : 1 px solid var ( - - border ) ; border-radius : 7 px ; padding : .2 rem .5 rem ; }
. prog-big { display : flex ; align-items : baseline ; gap : .5 rem ; }
@@ -68,15 +79,19 @@
<!-- ===== Rechte Spalte ===== -->
< aside class = "mf-rightbar" >
< div class = "mf-panel" >
< h4 > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M3 12h4l2-7 4 14 2-7h6" / > < / svg > System Status< / h4 >
< div class = "gaug e" >
< svg viewBox = "0 0 120 120 " fill = "none" >
< circle cx = "60" cy = "60" r = "50" stroke = "var(--line)" stroke-width = " 6"/ >
< circle cx = "60 " cy = "60 " r = "50 " stroke = "var(--accent)" stroke-width = "6 " stroke-linecap = "round" stroke-dasharray = "300 314 " transform = "rotate(-90 60 60) " style = "filter:drop-shadow(0 0 6px var(--glow-s)) " / >
< path d = "M37 52l17-13 20 8-8 21-22 8-12-15zM54 39l-2 22m22-14-22 14m-8 15 8-15m14 7-14-7" stroke = "var(--accent)" stroke-width = "2.2" stroke-linecap = "round" stroke-linejoin = "round" / >
< circle cx = "37" cy = "52" r = "3" fill = "var(--accent)" / > < circle cx = "54" cy = "39" r = "3" fill = "var(--accent)" / > < circle cx = "74" cy = "47" r = "3" fill = "var(--accent)" / > < circle cx = "66" cy = "68" r = "3" fill = "var(--accent)" / > < circle cx = "44" cy = "76" r = "3" fill = "var(--accent)" / > < circle cx = "52" cy = "61" r = "3" fill = "var(--accent)" / >
< h4 > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M3 12h4l2-7 4 14 2-7h6" / > < / svg > Lernnetz- Status< / h4 >
< div class = "network-status" id = "platformStatus" aria-live = "polit e">
< svg viewBox = "0 0 64 64 " fill = "none" stroke = "currentColor" stroke-width = "2.4" stroke-linecap = "round" stroke-linejoin = "round" aria-hidden = "true" >
< path d = "M15 26 29 14l18 8-5 20-20 8-9-13zM29 14l-2 22m20-14-20 14m-5 14 5-14m15 6-15- 6" / >
< circle cx = "15 " cy = "2 6" r = "3 " fill = "currentColor" / > < circle cx = "29 " cy = "14" r = "3" fill = "currentColor" / > < circle cx = "47" cy = "22" r = "3" fill = "currentColor" / > < circle cx = "42" cy = "42" r = "3 " fill = "currentColor" / > < circle cx = "22" cy = "50 " r = "3" fill = "currentColor" / > < circle cx = "27" cy = "36" r = "3" fill = "currentColor "/ >
< / svg >
< div class = "ok" > ALLE SYSTEME STABIL < / div > < div class = "op" > 100% OPERATIONAL < / div >
< div > < div class = "status-summary" data-status-summary > Lernnetz wird geprüft < / div > < p class = "status-caption" data-status-caption > Prüfe Fortschrittsspeicher und Lernmodule … < / p > < / div >
< div class = "status-details" data-status-details >
< div > < span > Fortschrittsspeicher< / span > < b data-status-storage > Wird geprüft< / b > < / div >
< div > < span > Lernmodule< / span > < b data-status-modules > Wird geprüft< / b > < / div >
< div > < span > Letzte Prüfung< / span > < b data-status-checked > – < / b > < / div >
< / div >
< button class = "status-toggle" type = "button" data-status-toggle aria-expanded = "false" > Details anzeigen< / button >
< / div >
< / div >
@@ -156,6 +171,45 @@
const points = values . map ( ( value , index ) => ` ${ Math . round ( index * 40 ) } ${ 64 - Math . round ( value / max * 54 ) } ` ) ;
const line = ` M ${ points . join ( ' L' ) } ` ;
weekChart . querySelector ( 'svg' ) . innerHTML = ` <defs><linearGradient id="ar" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="var(--accent)" stop-opacity=".35"/><stop offset="1" stop-color="var(--accent)" stop-opacity="0"/></linearGradient></defs><path d=" ${ line } L240 74 L0 74 Z" fill="url(#ar)"/><path d=" ${ line } " fill="none" stroke="var(--accent)" stroke-width="2" style="filter:drop-shadow(0 0 5px var(--glow-s))"/> ` ;
// Der Status prüft den Fortschrittsspeicher serverseitig. Die Modulzahl ist
// der registrierte Lernkatalog, nicht ein erfundener Prozentwert.
const statusCard = document . getElementById ( 'platformStatus' ) ;
const statusSummary = statusCard . querySelector ( '[data-status-summary]' ) ;
const statusCaption = statusCard . querySelector ( '[data-status-caption]' ) ;
const statusStorage = statusCard . querySelector ( '[data-status-storage]' ) ;
const statusModules = statusCard . querySelector ( '[data-status-modules]' ) ;
const statusChecked = statusCard . querySelector ( '[data-status-checked]' ) ;
const details = statusCard . querySelector ( '[data-status-details]' ) ;
const toggle = statusCard . querySelector ( '[data-status-toggle]' ) ;
toggle . addEventListener ( 'click' , ( ) => {
const open = details . classList . toggle ( 'open' ) ;
toggle . setAttribute ( 'aria-expanded' , String ( open ) ) ;
toggle . textContent = open ? 'Details ausblenden' : 'Details anzeigen' ;
} ) ;
fetch ( '/api/platform/status' , { headers : { Accept : 'application/json' } } )
. then ( async response => ( { response , status : await response . json ( ) } ) )
. then ( ( { response , status } ) => {
const ready = response . ok && status . storageConnected ;
statusCard . classList . toggle ( 'ready' , ready ) ;
statusCard . classList . toggle ( 'restricted' , ! ready ) ;
statusSummary . textContent = ready ? 'Bereit für deine Session' : 'Lernnetz eingeschränkt' ;
statusCaption . textContent = ready
? 'Fortschritt wird sicher gespeichert.'
: 'Fortschritt kann gerade nicht zuverlässig gespeichert werden.' ;
statusStorage . textContent = ready ? 'Verbunden' : 'Nicht erreichbar' ;
statusModules . textContent = ` ${ status . registeredModules } registriert ` ;
statusChecked . textContent = new Date ( status . checkedAt ) . toLocaleTimeString ( 'de-DE' , { hour : '2-digit' , minute : '2-digit' } ) ;
} )
. catch ( ( ) => {
statusCard . classList . add ( 'restricted' ) ;
statusSummary . textContent = 'Statusprüfung nicht möglich' ;
statusCaption . textContent = 'Der aktuelle Speicherzustand konnte nicht bestätigt werden.' ;
statusStorage . textContent = 'Unbekannt' ;
statusModules . textContent = 'Nicht bestätigt' ;
} ) ;
< / script >
< / body >
< / html >