@@ -1123,24 +1123,24 @@ function fahrzeugMeldungRoh(art) {
return { ts : null , km : null } ;
}
/* App-eigene Ölwechsel-Prognose, eigenständig statt über die allgemeine
tagesleistung()/prognose()-Mischrechnung (die für Ölwechsel zu träge ist,
weil sie den langfristigen Durchschnitt seit dem allerersten Servicebuch-
eintrag mit einfließen lässt): die Berechnung beginnt bewusst komplett neu
ab dem Datum des letzten tatsächlich durchgeführten Ölwechsels.
/ * A p p - e i g e n e S e r v i c e - P r o g n o s e , e i g e n s t ä n d i g s t a t t ü b e r d i e a l l g e m e i n e
tagesleistung ( ) / prognose ( ) - Mischrechnung ( die dafür zu träge ist , weil sie
den langfristigen Durchschnitt seit dem allerersten Servicebucheintrag mit
einfließen lässt ) : die Berechnung beginnt bewusst komplett neu ab dem Datum
der letzten tatsächlich durchgeführten Arbeit dieser Art .
Logik (wie vorgegeben): km und Tage seit dem letzten Ölwechsel ergeben die
aktuelle Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
eingestellten Intervall (Kundeneigene Einstellung, siehe CAR.oel) ergibt
das ein Datum - gedeckelt auf spätestens das Intervall-Zeitlimit (z. B. 1
Jahr) nach dem letzten Ölwechsel, falls seither wenig gefahren wurde. */
function oelwechselPrognose ( ) {
Logik ( wie vorgegeben ) : km und Tage seit diesem Eintrag ergeben die aktuelle
Fahrleistung pro Tag ; hochgerechnet auf die restlichen km bis zum
eingestellten Intervall ergibt das ein Datum - gedeckelt auf spätestens das
Intervall - Zeitlimit ( z . B . 1 Jahr ) nach dem letzten Eintrag , falls seither
wenig gefahren wurde .
Gemeinsame Grundlage für Ölwechsel und Inspektion ( siehe die beiden
Aufrufer darunter ) - beide rechnen identisch , nur mit anderem Intervall . * /
function servicePrognose ( stichwort , intervallKm , intervallMonate ) {
if ( ! CAR . odoBekannt ) return null ;
const letzter = letzterEintrag ( "öl" ) ;
const letzter = letzterEintrag ( stichwort ) ;
if ( ! letzter ) return null ;
const oel = CAR . oel ;
const intervallKm = oel . modus === "hersteller" ? oel . herstellerKm : Math . min ( oel . km , oel . herstellerKm ) ;
const intervallMonate = oel . modus === "hersteller" ? oel . herstellerMonate : Math . min ( oel . monate , oel . herstellerMonate ) ;
const zielKm = letzter . km + intervallKm ;
const restKm = Math . max ( 0 , zielKm - CAR . odo ) ;
const kappDatum = monatePlus ( letzter . datum , intervallMonate ) ;
@@ -1151,13 +1151,13 @@ function oelwechselPrognose() {
const tageSeit = ( heute ( ) - dat ( letzter . datum ) ) / dTag ;
const kmSeit = CAR . odo - letzter . km ;
if ( tageSeit <= 0 || kmSeit <= 0 ) {
// Noch keine belastbare Fahrleistung seit dem Ölwechsel bekannt (z. B.
// Noch keine belastbare Fahrleistung seit dem Eintrag bekannt (z. B.
// direkt danach) - ohne Rate keine seriöse Km-Prognose, also das
// Zeitlimit als einzig belastbare Grenze zeigen.
datum = kappDatum ;
} else {
const kmProTagSeitOel = kmSeit / tageSeit ;
const tageBisZiel = restKm / kmProTagSeitOel ;
const kmProTagSeither = kmSeit / tageSeit ;
const tageBisZiel = restKm / kmProTagSeither ;
const berechnet = new Date ( heute ( ) . getTime ( ) + tageBisZiel * dTag ) ;
datum = berechnet < kappDatum ? berechnet : kappDatum ;
}
@@ -1165,6 +1165,27 @@ function oelwechselPrognose() {
return { km : zielKm , restKm , datum } ;
}
function oelwechselPrognose ( ) {
const oel = CAR . oel ;
return servicePrognose ( "öl" ,
oel . modus === "hersteller" ? oel . herstellerKm : Math . min ( oel . km , oel . herstellerKm ) ,
oel . modus === "hersteller" ? oel . herstellerMonate : Math . min ( oel . monate , oel . herstellerMonate ) ) ;
}
/* Inspektion: dieselben 30.000 km / 24 Monate wie in intervalle ( ) / termine ( ) .
Angezeigt wird davon bewusst nur das Datum ( "voraussichtlich am …" ) - die
Restkilometer stehen bei der Inspektion schon in der Zeile daneben . * /
function inspektionPrognose ( ) { return servicePrognose ( "inspektion" , 30000 , 24 ) ; }
/ * K l e i n e g r a u e Z w e i t z e i l e u n t e r d e m T e r m i n - T i t e l , i n S e r v i c e - K a c h e l u n d
Service - Seite identisch aufgebaut . Leer , solange kein passender
Servicebucheintrag vorliegt . * /
function prognoseZeile ( art ) {
const p = art === "Ölwechsel" ? oelwechselPrognose ( ) : art === "Inspektion" ? inspektionPrognose ( ) : null ;
if ( ! p ) return "" ;
const km = art === "Ölwechsel" ? de ( p . restKm ) + " km, " : "" ;
return ` <br><span style="color:var(--fg3);font-size:11px"> ${ km } voraussichtlich am ${ dedat ( p . datum ) } </span> ` ;
}
function terminText ( t ) {
const restKm = t . km !== null && CAR . odoBekannt ? t . km - CAR . odo : null ;
const restKmAbs = restKm !== null ? Math . abs ( restKm ) : null ;
@@ -1318,22 +1339,25 @@ function vAudi() {
// (Sensor nicht verfügbar), fällt sie auf dieselbe App-eigene
// Servicebuch-Schätzung zurück wie Hauptuntersuchung.
const fm = t . art === "Hauptuntersuchung" ? { ts : null , km : null } : fahrzeugMeldungRoh ( t . art ) ;
// Zusätzlich zur realen Meldung klein die App-eigene Prognose
// (Servicebuch + tatsächliche Fahrleistung seit der letzten Arbeit,
// siehe servicePrognose()). Steht links unter dem Kachel-Titel statt
// rechts bei "am"/"in", die beide gleichwertig (gleiche Schriftgröße
// wie "am") untereinander in der rechten Spalte stehen. Die Inspektion
// bekommt sie auf Wunsch auch ohne Fahrzeugmeldung und ohne den
// km-Anteil - beim Ölwechsel stünde die Zahl dort doppelt, weil
// terminText() den generischen Termin schon "voraussichtlich" nennt.
const eigen = prognoseZeile ( t . art ) ;
if ( fm . ts == null && fm . km == null ) {
const x = terminText ( t ) ;
return ` <div class="row" ${ letzte } >
< dt > $ { t . art } < / d t > < d d > $ { x . h a u p t } $ { x . n e b e n ? ` < s m a l l s t y l e = " t e x t - a l i g n : l e f t " > $ { x . n e b e n } < / s m a l l > ` : " " } < / d d > < / d i v > ` ;
< dt > $ { t . art } $ { t . art === "Inspektion" ? eigen : "" } < / d t >
< dd > $ { x . haupt } $ { x . neben ? ` <small style="text-align:left"> ${ x . neben } </small> ` : "" } < / d d > < / d i v > ` ;
}
const haupt = fm . ts ? dedat ( new Date ( fm . ts ) ) : "" ;
const neben = fm . km != null ? "in " + de ( fm . km ) + " km" : "" ;
// Zusätzlich zur realen Meldung bei Ölwechsel klein die App-eigene
// Prognose (Servicebuch + tatsächliche Fahrleistung seit dem letzten
// Wechsel, siehe oelwechselPrognose()) - bei Inspektion bewusst nicht,
// wie gefordert. Steht links unter dem Kachel-Titel statt rechts bei
// "am"/"in", die beide gleichwertig (gleiche Schriftgröße wie "am")
// untereinander in der rechten Spalte stehen.
const eigen = t . art === "Ölwechsel" ? oelwechselPrognose ( ) : null ;
return ` <div class="row" ${ letzte } >
<dt> ${ t . art } ${ eigen ? ` <br><span style="color:var(--fg3);font-size:11px"> ${ de ( eigen . restKm ) } km, voraussichtlich am ${ dedat ( eigen . datum ) } </span> ` : "" } </dt>
< dt > $ { t . art } $ { eigen } < / d t >
< dd > $ { haupt } $ { neben ? ` <span style="display:block"> ${ neben } </span> ` : "" } < / d d > < / d i v > ` ;
} ) . join ( "" ) }
< / d l > < / b u t t o n >
@@ -1628,11 +1652,12 @@ function vService() {
const anzeige = iv . hersteller || iv . eigen ;
const fm = fahrzeugMeldungRoh ( t . art ) ;
const fmDatum = fm . ts ? dedat ( new Date ( fm . ts ) ) : null ;
// Forecast-Zeile nur bei Ölwechsel (K undeneigenes Intervall aus
// "Einrichten", eigenständige Reset-nach-Service-Logik, siehe
// oe lwechselPrognose()) - bei Inspektion und Hauptuntersuchung
// bewusst keine Forecast-Zeile mehr .
// Forecast-Zeile aus dem k undeneigenen Intervall in "Einrichten" mit
// eigenständiger Reset-nach-Service-Logik (servicePrognose()): beim
// Ö lwechsel mit Restkilometern, bei der Inspektion auf Wunsch nur mit
// dem Datum. Hauptuntersuchung bleibt bewusst ohne .
const eigenOel = t . art === "Ölwechsel" ? oelwechselPrognose ( ) : null ;
const eigenInsp = t . art === "Inspektion" ? inspektionPrognose ( ) : null ;
return ` <div class="row" ${ letzte } >
< dt > $ { t . art }
$ { t . vereinbart ? ` <br><span style="color:var(--red);font-size:11px">Werkstatt am ${ dedat ( new Date ( t . vereinbart ) ) } </span> ` : "" }
@@ -1642,8 +1667,10 @@ function vService() {
$ { fm . km != null || fmDatum ? `
< span class = "werte" > $ { fm . km != null ? de ( fm . km ) + " km" : "" } $ { fm . km != null && fmDatum ? " / " : "" } $ { fmDatum || "" } < / s p a n >
$ { eigenOel ? ` <small style="text-align:left"> ${ de ( eigenOel . restKm ) } km, voraussichtlich am ${ dedat ( eigenOel . datum ) } </small> ` : "" }
$ { eigenInsp ? ` <small style="text-align:left">voraussichtlich am ${ dedat ( eigenInsp . datum ) } </small> ` : "" }
` : `
< span class = "werte" > $ { eigenOel ? de ( eigenOel . restKm ) + " km · " + dedat ( eigenOel . datum ) : ( anzeige . km !== null ? de ( anzeige . km ) + " km · " : "" ) + dedat ( anzeige . datum ) } < / s p a n >
$ { eigenInsp ? ` <small style="text-align:left">voraussichtlich am ${ dedat ( eigenInsp . datum ) } </small> ` : "" }
` }
< / d d >
< / d i v > ` ; } ) . j o i n ( " " ) }
@@ -2440,17 +2467,69 @@ function vReifen() {
$ { satz ( "winter" ) } ` ;
}
/ * G e m e i n s a m e s F o r m u l a r f ü r " N e u e F a h r t " u n d d a s B e a r b e i t e n e i n e r b e s t e h e n d e n
Fahrt - dieselben Felder an beiden Stellen , damit sie nicht auseinander -
laufen können ( gleiches Muster wie tankFelder ( ) / tankFormularWerte ( ) ) . Die
Felder decken genau das ab , was die Einzelfahrt - Seite anzeigt ; Verbrauch ,
Status und km - Quelle bleiben außen vor , die meldet das Fahrzeug bzw . leitet
das Backend ab . * /
const FAHRT _ARTEN = [ "privat" , "arbeitsweg" ] ;
/ * D a t u m f ü r e i n < i n p u t t y p e = " d a t e " > i n O r t s z e i t . B e w u s s t n i c h t ü b e r
toISOString ( ) , das rechnet nach UTC um und liefert kurz nach Mitternacht
den Vortag . * /
function datumsfeld ( d ) {
const zwei = ( n ) => String ( n ) . padStart ( 2 , "0" ) ;
return ` ${ d . getFullYear ( ) } - ${ zwei ( d . getMonth ( ) + 1 ) } - ${ zwei ( d . getDate ( ) ) } ` ;
}
function zeitfeld ( d ) {
const zwei = ( n ) => String ( n ) . padStart ( 2 , "0" ) ;
return ` ${ zwei ( d . getHours ( ) ) } : ${ zwei ( d . getMinutes ( ) ) } ` ;
}
function fahrtFelder ( praefix , t ) {
const start = t && t . ts _start ? new Date ( t . ts _start ) : new Date ( ) ;
const dauer = t && t . duration _s != null ? Math . round ( t . duration _s / 60 ) : "" ;
const art = t && t . art ? t . art : FAHRT _ARTEN [ 0 ] ;
const wert = ( v ) => ( v == null ? "" : v ) ;
return `
< div class = "feld" > < label for = "${praefix}Datum" > Datum < / l a b e l > < i n p u t t y p e = " d a t e " i d = " $ { p r a e f i x } D a t u m " v a l u e = " $ { d a t u m s f e l d ( s t a r t ) } " > < / d i v >
< div class = "feld" > < label for = "${praefix}Start" > Startzeit < / l a b e l > < i n p u t t y p e = " t i m e " i d = " $ { p r a e f i x } S t a r t " v a l u e = " $ { t ? z e i t f e l d ( s t a r t ) : " 0 8 : 0 0 " } " > < / d i v >
< div class = "feld" > < label for = "${praefix}Dauer" > Dauer < / l a b e l >
< span class = "mitEinheit" > < input type = "number" min = "0" id = "${praefix}Dauer" value = "${dauer}" placeholder = "0" > < span > min < / s p a n > < / s p a n > < / d i v >
< div class = "feld" > < label for = "${praefix}Art" > Art < / l a b e l >
< select id = "${praefix}Art" > $ { FAHRT _ARTEN . map ( ( a ) => ` <option ${ a === art ? "selected" : "" } > ${ a } </option> ` ) . join ( "" ) } < / s e l e c t > < / d i v >
< div class = "feld" > < label for = "${praefix}Startort" > Startort < / l a b e l > < i n p u t i d = " $ { p r a e f i x } S t a r t o r t " v a l u e = " $ { e s c ( w e r t ( t & & t . s t a r t _ a d d r e s s ) ) } " p l a c e h o l d e r = " u n b e k a n n t " > < / d i v >
< div class = "feld" > < label for = "${praefix}Zielort" > Zielort < / l a b e l > < i n p u t i d = " $ { p r a e f i x } Z i e l o r t " v a l u e = " $ { e s c ( w e r t ( t & & t . e n d _ a d d r e s s ) ) } " p l a c e h o l d e r = " u n b e k a n n t " > < / d i v >
< div class = "feld" > < label for = "${praefix}OdoStart" > Startkilometer < / l a b e l > < i n p u t t y p e = " n u m b e r " i d = " $ { p r a e f i x } O d o S t a r t " v a l u e = " $ { w e r t ( t & & t . o d o _ s t a r t ) } " p l a c e h o l d e r = " w i r d e r g ä n z t " > < / d i v >
< div class = "feld" > < label for = "${praefix}OdoEnde" > Endkilometer < / l a b e l > < i n p u t t y p e = " n u m b e r " i d = " $ { p r a e f i x } O d o E n d e " v a l u e = " $ { w e r t ( t & & t . o d o _ e n d ) } " p l a c e h o l d e r = " w i r d e r g ä n z t " > < / d i v >
< div class = "feld" style = "border-bottom:0" > < label for = "${praefix}Distanz" > Gefahrene Distanz < / l a b e l >
< span class = "mitEinheit" > < input type = "number" step = "0.1" id = "${praefix}Distanz" value = "${wert(t && t.distance_km)}" placeholder = "aus km-Stand" > < span > km < / s p a n > < / s p a n > < / d i v > ` ;
}
function fahrtFormularWerte ( praefix ) {
const zahl = ( id ) => { const v = $ ( ` # ${ praefix } ${ id } ` ) . value ; return v === "" ? null : parseFloat ( v . replace ( "," , "." ) ) ; } ;
const text = ( id ) => $ ( ` # ${ praefix } ${ id } ` ) . value . trim ( ) || null ;
const dauer = parseInt ( $ ( ` # ${ praefix } Dauer ` ) . value , 10 ) || 0 ;
const tsStart = new Date ( ` ${ $ ( ` # ${ praefix } Datum ` ) . value } T ${ $ ( ` # ${ praefix } Start ` ) . value } :00 ` ) ;
return {
ts _start : tsStart . toISOString ( ) ,
ts _end : new Date ( tsStart . getTime ( ) + dauer * 60000 ) . toISOString ( ) ,
art : $ ( ` # ${ praefix } Art ` ) . value ,
start _ort : text ( "Startort" ) , ziel _ort : text ( "Zielort" ) ,
odo _start : zahl ( "OdoStart" ) , odo _end : zahl ( "OdoEnde" ) , distanz : zahl ( "Distanz" ) ,
} ;
}
function vTripsFormular ( ) {
// eigene Funktion, damit sowohl der volle als auch der Leer-Zustand
// (§11 Abnahmekriterium 1: eine Fahrt muss erfasst werden können) dasselbe
// Formular zeigen - vorher fehlte es im Leer-Zustand komplett, der Knopf
// wechselte zu "Abbrechen", aber es erschienen keine Felder.
return ` ${ fahrtOffen ? ` <div class="tile"><span class="label">Neue Fahrt</span>
<div class="feld"><label>Datum</label><input type="date" id="neuFahrtDatum" value=" ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } "></div>
<div class="feld"><label>Startzeit</label><input type="time" id="neuFahrtStart" value="08:00"></div>
<div class="feld" style="border-bottom:0"><label>Dauer in Minuten</label><input type="number" id="neuFahrtDauer" placeholder="0"></div>
<button class="aktion" data-fahrtspeichern>Speichern</button></div> ` : "" }
<button class="aktion primaer" data-fahrt> ${ fahrtOffen ? "Abbrechen" : "Fahrt erfassen" } </button> ` ;
$ { fahrtFelder ( "neuFahrt" , null ) }
< button class = "aktion primaer" data - fahrtspeichern > Speichern < / b u t t o n > < / d i v > ` : " " }
< button class = "aktion ${fahrtOffen ? " " : " primaer "}" data - fahrt > $ { fahrtOffen ? "Abbrechen" : "Fahrt erfassen" } < / b u t t o n > ` ;
}
function vTrips ( ) {
const jahre = [ ... new Set ( TRIPS . map ( ( t ) => new Date ( t . ts _start ) . getFullYear ( ) ) ) ] . sort ( ( a , b ) => b - a ) ;
@@ -2491,6 +2570,16 @@ function vTrips() {
function vTrip ( id ) {
const t = TRIPS . find ( ( x ) => x . trip _id === id ) ;
if ( ! t ) return ` <div class="tile">Fahrt nicht gefunden</div> ` ;
if ( fahrtBearbeitenOffen ) {
return `
< div class = "tile" > < span class = "label" > Fahrt bearbeiten < / s p a n >
$ { fahrtFelder ( "bearbFahrt" , t ) }
< button class = "aktion primaer" data - fahrtaktualisieren = "${t.trip_id}" > Speichern < / b u t t o n >
< button class = "aktion" data - fahrtbearbeiten > Abbrechen < / b u t t o n >
< / d i v > ` ;
}
return `
$ { mapBox ( ) }
< div class = "tile flat" style = "margin:18px 0 14px" >
@@ -2509,7 +2598,9 @@ function vTrip(id) {
< div class = "row" > < dt > Dauer < /dt><dd>${Math.round(t.duration_s / 60 ) } min < / d d > < / d i v >
< div class = "row" > < dt > Verbrauch < / d t > < d d > $ { f a h r t V e r b r a u c h ( t ) ! = n u l l ? d e ( f a h r t V e r b r a u c h ( t ) , 1 ) + " l / 1 0 0 k m " : " l i e g t n i c h t v o r " } < s m a l l > v o m F a h r z e u g g e m e l d e t < / s m a l l > < / d d > < / d i v >
< div class = "row" > < dt > Status < / d t > < d d > $ { t . s t a t u s } < s m a l l > Q u e l l e : $ { t . k m _ q u e l l e | | " – " } < / s m a l l > < / d d > < / d i v >
</dl></div>
< / d l >
< button class = "aktion" data - fahrtbearbeiten > Werte bearbeiten < / b u t t o n >
< / d i v >
< button class = "aktion loeschen" data - tripdel = "${t.trip_id}" > Fahrt löschen < / b u t t o n > ` ;
}
@@ -2637,6 +2728,7 @@ function vStat() {
let neuOffen = false ;
let fahrtOffen = false ;
let tankBearbeitenOffen = false ;
let fahrtBearbeitenOffen = false ;
// Ergebnis eines geparsten, noch nicht gespeicherten Belegs: { praefix, daten }
// - überlagert in tankFelder() die Beleg-Felder des jeweils offenen Formulars.
let belegEntwurf = null ;
@@ -3324,6 +3416,9 @@ const go = (name, id) => {
entwurfIndex = null ;
}
sheet = null ;
// Ein offenes Bearbeiten-Formular gehört zu genau der Fahrt, auf der es
// geöffnet wurde - sonst begrüßt die nächste Fahrt einen mit Eingabefeldern.
fahrtBearbeitenOffen = false ;
/ * B 3 : e c h t e r V e r l a u f s t a t t s t a t i s c h e r Z U R U E C K - T a b e l l e . E i n T a b w e c h s e l s e t z t
den Stapel zurück , weil Tabs Wurzeln sind , keine Ebenen . * /
if ( istTab ( name ) ) routeStack = [ ] ;
@@ -3333,6 +3428,7 @@ const go = (name, id) => {
} ;
function zurueck ( ) {
sheet = null ;
fahrtBearbeitenOffen = false ;
const ziel = routeStack . pop ( ) || { name : ZURUECK [ route . name ] || "home" } ;
navRichtung = "raus" ;
route = ziel ; render ( ) ;
@@ -3821,12 +3917,18 @@ function ereignisseVerdrahten() {
}
if ( e . target . closest ( "[data-fahrt]" ) ) { fahrtOffen = ! fahrtOffen ; render ( ) ; return ; }
if ( e . target . closest ( "[data-fahrtspeichern]" ) ) {
const datum = $ ( "#neuFahrtDatum" ) . value , start = $ ( "#neuFahrtStart" ) . value , dauer = parseInt ( $ ( "# neuFahrtDauer" ) . value , 10 ) || 0 ;
const tsStart = new Date ( ` ${ datum } T ${ start } :00 ` ) ;
const tsEnd = new Date ( tsStart . getTime ( ) + dauer * 60000 ) ;
serviceRufen ( "audi_dashboard_fahrt_manuell_anlegen" , { ts _start : tsStart . toISOString ( ) , ts _end : tsEnd . toISOString ( ) , art : "privat" } ) ;
serviceRufen ( "audi_dashboard_fahrt_manuell_anlegen" , fahrtFormularWerte ( "neuFahrt" ) ) ;
fahrtOffen = false ; render ( ) ; return ;
}
if ( e . target . closest ( "[data-fahrtbearbeiten]" ) ) {
fahrtBearbeitenOffen = ! fahrtBearbeitenOffen ; render ( ) ; return ;
}
const fahrtAkt = e . target . closest ( "[data-fahrtaktualisieren]" ) ;
if ( fahrtAkt ) {
serviceRufen ( "audi_dashboard_fahrt_aktualisieren" ,
{ trip _id : fahrtAkt . dataset . fahrtaktualisieren , ... fahrtFormularWerte ( "bearbFahrt" ) } ) ;
fahrtBearbeitenOffen = false ; render ( ) ; return ;
}
if ( e . target . closest ( "[data-tankspeichern]" ) ) {
const w = tankFormularWerte ( "neuTank" ) ;
if ( w . liter > 0 && w . kosten > 0 ) {
@@ -3987,7 +4089,14 @@ function ereignisseVerdrahten() {
const a = e . target . closest ( "[data-acc]" ) ;
if ( a ) { const b = ROOT . getElementById ( a . dataset . acc ) ; a . setAttribute ( "aria-expanded" , b . classList . toggle ( "on" ) ) ; return ; }
const art = e . target . closest ( "[data-art]" ) ;
if ( art ) { const tr = TRIPS . find ( ( x ) => x . trip _id === art . dataset . art ) ; tr . art = tr . art === "arbeitsweg" ? "privat" : "arbeitsweg" ; render ( ) ; return ; }
if ( art ) {
const tr = TRIPS . find ( ( x ) => x . trip _id === art . dataset . art ) ;
tr . art = tr . art === "arbeitsweg" ? "privat" : "arbeitsweg" ;
// Bis 2026-08-17 blieb das Umschalten rein lokal und war nach dem
// naechsten Backend-Push wieder weg - jetzt wandert es mit.
serviceRufen ( "audi_dashboard_fahrt_aktualisieren" , { trip _id : tr . trip _id , art : tr . art } ) ;
render ( ) ; return ;
}
// Standort (Vollbild) - Karten-/Menü-Bedienung manipuliert SMAP/das Menü-
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
if ( e . target . closest ( "[data-standort-umschalten]" ) ) { standortMenuUmschalten ( ) ; return ; }
@@ -4209,8 +4318,15 @@ function belegErgebnisVerarbeiten(daten) {
Deshalb jetzt ohne festes Abbruchlimit : nach der anfänglichen schnellen
Serie ( siehe NACHLADE _INTERVALL _MS ) läuft es mit einem größeren , aber
endlosen Intervall weiter , bis Daten da sind oder das Element abgehängt
wird (disconnectedCallback stoppt hier nichts explizit, aber ein
getimeter Aufruf ohne HASS/callWS bricht oben ohnehin ab). */
wird ( disconnectedCallback stoppt den Timer , connectedCallback stößt ihn
über datenLaden ( ) wieder an ) .
2026 - 08 - 17 : Der Takt darf an keiner Stelle abreißen . Vorher stieg sowohl
nachladeAnstossen ( ) ( bei fehlendem HASS / callWS ) als auch datenLaden ( ) ( bei
fehlendem HASS ) still aus , ohne eine nächste Runde zu planen - danach half
nur noch ein zufälliger hass - Push oder ein Klick ins Menü ( go ( ) ruft
datenLaden ( ) mit auf ) . Genau das war das gemeldete " Lädt … bis man auf
einen Menüpunkt klickt " . * /
const NACHGELADEN = { } ;
let nachladeTimer = null ;
let nachladeVersuche = 0 ;
@@ -4219,7 +4335,7 @@ const NACHLADE_LANGSAM_MS = 10000;
const NACHLADE _SCHNELLE _VERSUCHE = 15 ;
function nachladeAnstossen ( ) {
if ( nachladeTimer || ! HASS || ! HASS . callWS ) return ;
if ( nachladeTimer ) return ;
nachladeVersuche ++ ;
// Erster Versuch fast sofort statt erst nach vollem Intervall - der
// Wettlauf mit pyscripts beim_start() (siehe Kopfkommentar) ist meist
@@ -4232,26 +4348,40 @@ function nachladeAnstossen() {
: NACHLADE _LANGSAM _MS ;
nachladeTimer = setTimeout ( async ( ) => {
nachladeTimer = null ;
try {
const states = await HASS . callWS ( { type : "get_states" } ) ;
const finden = ( id ) => states . find ( ( s ) => s . entity _id === id ) ;
NACHGELADEN . profil = finden ( "pyscript.audi_dashboard_profil" ) || NACHGELADEN . profil ;
NACHGELADEN . fahrten = finden ( "pyscript.audi_dashboard_fahrten" ) || NACHGELADEN . fahrten ;
NACHGELADEN . tank = finden ( "pyscript.audi_dashboard_tankvorgaenge" ) || NACHGELADEN . tank ;
NACHGELADEN . status = finden ( "pyscript.audi_dashboard_fahrzeugstatus" ) || NACHGELADEN . status ;
NACHGELADEN . batt = finden ( "pyscript.audi_dashboard_batterieverlauf" ) || NACHGELADEN . batt ;
// Ohne diese Zeile bleibt der Setup-Katalog nach einem HA-Neustart leer,
// bis zufaellig ein Push kommt - siehe setupBereit().
NACHGELADEN . entitaeten = finden ( "pyscript.audi_dashboard_entitae ten" ) || NACHGELADEN . entitaeten ;
} catch ( e ) {
// wird beim naechsten Versuch erneut probiert
// HASS kann hier noch fehlen: panel_custom erzeugt das Element und setzt
// hass nicht immer in dieser Reihenfolge. Frueher brach die Schleife in so
// einem Fall oben ganz ab (Bedingung !HASS || !HASS.callWS) und wurde nie
// wieder angestossen - genau der dauerhaft haengende "Lädt …"-Bildschirm.
// Jetzt wird die Runde nur uebersprungen, der Takt laeuft weiter.
if ( HASS && HASS . callWS ) {
try {
const states = await HASS . callWS ( { type : "get_states" } ) ;
const finden = ( id ) => states . find ( ( s ) => s . entity _id === id ) ;
NACHGELADEN . profil = finden ( "pyscript.audi_dashboard_profil" ) || NACHGELADEN . profil ;
NACHGELADEN . fahrten = finden ( "pyscript.audi_dashboard_fahr ten" ) || NACHGELADEN . fahrten ;
NACHGELADEN . tank = finden ( "pyscript.audi_dashboard_tankvorgaenge" ) || NACHGELADEN . tank ;
NACHGELADEN . status = finden ( "pyscript.audi_dashboard_fahrzeugstatus" ) || NACHGELADEN . status ;
NACHGELADEN . batt = finden ( "pyscript.audi_dashboard_batterieverlauf" ) || NACHGELADEN . batt ;
// Ohne diese Zeile bleibt der Setup-Katalog nach einem HA-Neustart leer,
// bis zufaellig ein Push kommt - siehe setupBereit().
NACHGELADEN . entitaeten = finden ( "pyscript.audi_dashboard_entitaeten" ) || NACHGELADEN . entitaeten ;
} catch ( e ) {
// wird beim naechsten Versuch erneut probiert
}
}
datenLaden ( true ) ;
// Rueckversicherung: datenLaden() stoesst die naechste Runde selbst an,
// aber nur, wenn es nicht vorher aussteigt. Der Takt darf an keiner
// Stelle abreissen, solange noch keine Daten da sind.
if ( ! DATEN _GELADEN ) nachladeAnstossen ( ) ;
} , verzoegerung ) ;
}
async function datenLaden ( erzwingen ) {
if ( ! HASS ) return ;
if ( ! HASS ) {
if ( ! DATEN _GELADEN ) nachladeAnstossen ( ) ;
return ;
}
const profilState = HASS . states [ "pyscript.audi_dashboard_profil" ] || NACHGELADEN . profil ;
const fahrtenState = HASS . states [ "pyscript.audi_dashboard_fahrten" ] || NACHGELADEN . fahrten ;
const tankState = HASS . states [ "pyscript.audi_dashboard_tankvorgaenge" ] || NACHGELADEN . tank ;
@@ -4355,6 +4485,9 @@ class AudiDashboardPanel extends HTMLElement {
}
disconnectedCallback ( ) {
// Nachlade-Takt anhalten, solange das Panel nicht im Dokument haengt -
// connectedCallback() stoesst ihn ueber datenLaden() wieder an.
if ( nachladeTimer ) { clearTimeout ( nachladeTimer ) ; nachladeTimer = null ; }
// Alle drei Karten, nicht nur MAP: sonst bleibt beim Abhaengen des Panels
// (Standort- oder Uebersichtsseite offen) eine Leaflet-Instanz samt ihrem
// window-Resize-Horcher und laufenden Kachelabrufen am Leben.