Teilen-Blatt, HIG-Blaetter, geteilte Ansichtseinstellungen (2026.8.31.1)

Share-Erweiterung: ein PDF aus Mail landet ueber Teilen -> DataMetric360 als
Tankbeleg. Alles Native liegt versioniert unter companion-app/native/, weil
ios/ gitignored ist und npx cap add ios es sonst wieder frisst;
scripts/ios-teilen-einrichten.mjs haengt es bei jedem Bau ins Xcode-Projekt
und ist in ios-signieren.sh eingehaengt. Der pbxproj-Teil ist auf keinem Mac
erprobt - er bricht vor dem Schreiben ab, und docs/SHARE_EXTENSION.md
beschreibt dieselben Handgriffe fuer Xcode.

"In den Kalender uebernehmen" ging in der App nie: WKWebView ignoriert das
download-Attribut, der Klick lief ins Leere. Auf dem Geraet jetzt
@capacitor/filesystem plus @capacitor/share.

useTheme/useBoden legten je Aufrufer eigenen Zustand an - fuenf Kopien. Der
Schalter aenderte nur seine eigene, data-theme in App.tsx blieb stehen, die
Tag/Nacht-Umschaltung wirkte erst nach einem Neustart. Jetzt ein Speicher je
Einstellung ueber useSyncExternalStore. Zwei Regressionstests, gegen den
alten Stand als fehlschlagend nachgewiesen.

Alle Popups nach Apple HIG: neue Bausteine Sheet und ActionSheet, blickdicht
und mit abgedunkeltem Schleier, in beiden Codebasen. Popup portalierte nach
document.body - ausserhalb von [data-theme] - und war deshalb im Tagmodus
fast unsichtbar; Portal zielt jetzt auf .ads-root.

ImagePlaceholder merkte sich "fehlgeschlagen" ohne die Adresse: nach einem
leeren Galerieplatz zeigten auch die mit Foto nur noch den Platzhalter.
Vier Regressionstests, ebenfalls gegen den alten Stand geprueft.

Bildadressen tragen jetzt einen Cache-Brecher - HA liefert /local/ mit 31
Tagen Cache-Vorgabe aus, im Panel eingefuegte Radfotos erschienen in der App
deshalb nicht.

uebersichtsbild: Panel speichert den Namen, die App verglich gegen den
Dateinamen - der Vergleich traf nie zu. Kanonisch ist der Name.

Weiter: Navigationspfeil statt gleichschenkligem Dreieck auf der Streckenlinie
(die Kerbe unterscheidet Kopf und Ende), CI-Symbol tour-s als Fahrten-Icon,
Datumsfelder zeigen ihren Wert ohne erstes Antippen, feste Beispielnamen im
Setup, Kopf-Gegengewicht fuer mittige Titel, Standort-Blatt mit festem
Fussabstand, NSLocationWhenInUseUsageDescription, watchPosition fuer die
Live-Ortung, Art-Pille wieder als Knopf, Zwischenablage fuer Tankbelege.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-31 14:24:52 +02:00
co-authored by Claude Opus 5
parent 26f72930c3
commit b1411625aa
58 changed files with 2846 additions and 368 deletions
@@ -573,6 +573,82 @@ function tiles() {
if (TILE) MAP.removeLayer(TILE);
TILE = window.L.tileLayer(url, { maxZoom: 19, attribution: ATTR }).addTo(MAP);
}
/* Fahrtrichtung auf der Streckenlinie.
Bis 2026-08-31 lagen auf der Linie kleine runde Punkte - je einer pro
GPS-Stuetzpunkt. Sie sagten nichts ueber die Richtung, und bei einer Fahrt
hin und zurueck auf derselben Strasse verschmolzen Hin- und Rueckweg zu
einem einzigen roten Band: aus der Karte war nicht mehr abzulesen, dass es
ueberhaupt zwei Wege waren.
Die Form ist der Navigationspfeil, kein gleichschenkliges Dreieck: das
Dreieck sieht von beiden Seiten gleich aus und laesst auf Abstand offen,
wo vorn ist - besonders dort, wo die Strecke die Richtung wechselt. Die
Kerbe im Fuss macht Kopf und Ende unterscheidbar (Nutzervorgabe mit
Vorlage, 2026-08-31).
Drei Entscheidungen, jede an Mustern nachgemessen (siehe unten):
1. Abstand nach STRECKE, nicht nach Stuetzpunkten. Die Punktdichte des
Rohtracks haengt an der Geschwindigkeit - im Stau entsteht ein Haufen,
auf der Autobahn eine Luecke. Pfeile daran haetten genau dort geklumpt,
wo am wenigsten passiert ist.
2. Mittig auf der Linie, ohne Versatz quer zur Fahrtrichtung. Ein solcher
Versatz war erprobt und trennt eine Hin-und-Rueck-Strecke sauber auf
zwei Spuren - aber nur, wenn Hin- und Rueckweg wirklich Punkt fuer Punkt
uebereinanderliegen. Das tun echte GPS-Aufzeichnungen nicht (Eigentum
des Fahrzeughalters, 2026-08-31). Fuer einen Fall, der in den Daten
nicht vorkommt, waere die schiefe Lage aller Dreiecke ein taeglicher
Preis ohne Gegenwert.
3. Weisser Rand. Das Dreieck liegt in derselben Farbe auf derselben Linie;
ohne die helle Kontur verschwimmt beides zu einer dicken Wurst. */
function spurMeter(a, b) {
const R = 6371000, r = Math.PI / 180;
const dLat = (b[0] - a[0]) * r, dLon = (b[1] - a[1]) * r;
const h = Math.sin(dLat / 2) ** 2 + Math.cos(a[0] * r) * Math.cos(b[0] * r) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
}
/* Winkel im Uhrzeigersinn ab Norden - genau das, was CSS rotate() erwartet.
Die Laengendifferenz wird mit cos(Breite) gestaucht, sonst zeigten die
Pfeile in unseren Breiten rund 30 Grad daneben. */
function spurWinkel(a, b) {
const dx = (b[1] - a[1]) * Math.cos(((a[0] + b[0]) / 2) * Math.PI / 180);
return Math.atan2(dx, b[0] - a[0]) * 180 / Math.PI;
}
function spurPfeile(karte, spur, farbe) {
if (!Array.isArray(spur) || spur.length < 2 || !window.L) return;
const laengen = [];
let gesamt = 0;
for (let i = 1; i < spur.length; i++) { const d = spurMeter(spur[i - 1], spur[i]); laengen.push(d); gesamt += d; }
if (!gesamt) return;
// Rund alle 350 m, aber nie weniger als 4 und nie mehr als 14: auf einer
// 200-km-Fahrt waeren 570 Pfeile eine zweite Linie, auf 300 m gar keiner.
const anzahl = Math.max(4, Math.min(14, Math.round(gesamt / 350)));
const schritt = gesamt / (anzahl + 1);
let ziel = schritt, gelaufen = 0;
for (let i = 1; i < spur.length && ziel < gesamt; i++) {
const d = laengen[i - 1];
while (ziel <= gelaufen + d && ziel < gesamt) {
const f = d ? (ziel - gelaufen) / d : 0;
const lat = spur[i - 1][0] + (spur[i][0] - spur[i - 1][0]) * f;
const lon = spur[i - 1][1] + (spur[i][1] - spur[i - 1][1]) * f;
const w = spurWinkel(spur[i - 1], spur[i]).toFixed(1);
const html = '<div style="transform:rotate(' + w + 'deg)">'
+ '<svg width="13" height="13" viewBox="0 0 10 10" style="overflow:visible">'
+ '<path d="M5 0.4 L8.8 9.6 L5 6.4 L1.2 9.6 Z" fill="none" stroke="#fff" stroke-width="1.8" stroke-linejoin="round" opacity=".95"/>'
+ '<path d="M5 0.4 L8.8 9.6 L5 6.4 L1.2 9.6 Z" fill="' + farbe + '"/></svg></div>';
window.L.marker([lat, lon], {
icon: window.L.divIcon({ className: "spur-pfeil", html, iconSize: [13, 13], iconAnchor: [6.5, 6.5] }),
interactive: false,
}).addTo(karte);
ziel += schritt;
}
gelaufen += d;
}
}
async function initMap() {
await leafletLaden();
const el = ROOT.getElementById("map");
@@ -591,7 +667,10 @@ async function initMap() {
? t.route
: [[t.start_lat, t.start_lon], [t.end_lat, t.end_lon]];
const line = window.L.polyline(track, { color: rot, weight: 2.5, opacity: 0.9, dashArray: Array.isArray(t.route) && t.route.length >= 2 ? null : "4 6" }).addTo(MAP);
track.slice(1, -1).forEach((pt) => window.L.circleMarker(pt, { radius: 2.5, color: rot, weight: 0, fillColor: rot, fillOpacity: 0.85 }).addTo(MAP));
// Nur auf einer echten Aufzeichnung: die gestrichelte Ersatzlinie
// zwischen Start und Ziel ist keine gefahrene Strecke, und ein Pfeil
// darauf behauptete eine Fahrtrichtung, die niemand gemessen hat.
if (Array.isArray(t.route) && t.route.length >= 2) spurPfeile(MAP, track, rot);
window.L.circleMarker([t.start_lat, t.start_lon], { radius: 6, color: "#fff", weight: 2, fillColor: "#000", fillOpacity: 1 }).addTo(MAP).bindPopup("Start · " + (t.start_address || ""));
window.L.circleMarker([t.end_lat, t.end_lon], { radius: 6, color: rot, weight: 2, fillColor: rot, fillOpacity: 1 }).addTo(MAP).bindPopup("Ziel · " + (t.end_address || ""));
MAP.fitBounds(line.getBounds(), { padding: [26, 26] });
@@ -637,6 +716,7 @@ async function initMap() {
let USER_POS = null; // {lat, lon, genauigkeit} oder null
let USER_POS_TS = 0;
let USER_POS_FEHLER = null;
let USER_WATCH = null; // watchPosition-Kennung, solange die Standortseite offen ist
let STANDORT_ADRESSE = null; // zuletzt aufgelöste Adresse (Cache über Koordinaten-Rundung)
let STANDORT_ADRESSE_KEY = null;
let STANDORTMENU_OFFEN = false;
@@ -779,6 +859,43 @@ function standortErfassenFallsNoetig() {
standortErfassen();
}
/* Fortlaufende Ortung, solange die Standortseite offen ist - "live bei
Verwendung". Die einmalige Abfrage oben bleibt daneben bestehen und ist
weiterhin das Richtige fuer die Uebersichtskachel: die zeigt nur einen
Abstand, und eine dauerhafte Hochgenauigkeits-Ortung auf der Startseite
waere Batterieverbrauch ohne Gegenwert.
Beendet wird die Verfolgung an denselben zwei Stellen, an denen auch SMAP
abgeraeumt wird - sonst liefe sie nach dem Verlassen der Seite unbemerkt
weiter. Genau das verlangt Apples Vorgabe fuer "Beim Verwenden der App":
erfassen, solange der Nutzer die Karte ansieht, und keine Sekunde laenger. */
function standortVerfolgungStarten() {
if (USER_WATCH !== null) return;
if (!navigator.geolocation) { USER_POS_FEHLER = "GPS offline"; standortAnsichtAktualisieren(); return; }
USER_WATCH = navigator.geolocation.watchPosition(
(pos) => {
USER_POS = { lat: pos.coords.latitude, lon: pos.coords.longitude, genauigkeit: pos.coords.accuracy };
USER_POS_TS = Date.now(); USER_POS_FEHLER = null;
standortAnsichtAktualisieren();
},
(err) => {
// Dieselbe Unterscheidung wie in standortErfassen(): die verweigerte
// Freigabe ist der einzige Fall mit einer anderen Abhilfe.
USER_POS_FEHLER = err.code === err.PERMISSION_DENIED ? "GPS-Freigabe fehlt" : "GPS offline";
USER_POS_TS = Date.now();
standortAnsichtAktualisieren();
},
// maximumAge klein: eine zwischengespeicherte Position aus der letzten
// halben Minute waere hier das Gegenteil von "live".
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 2000 }
);
}
function standortVerfolgungBeenden() {
if (USER_WATCH === null) return;
navigator.geolocation.clearWatch(USER_WATCH);
USER_WATCH = null;
}
/* Reverse-Geocoding über die öffentliche Nominatim-API (OpenStreetMap) -
wie die Kartenkacheln oben (TILE_URL) ohne eigenen Schlüssel; bei diesem
Aufrufvolumen (ein Fahrzeug, nur bei geöffneter Standortansicht) im Rahmen
@@ -961,7 +1078,11 @@ function standortMarkerZeichnen() {
}
function standortAufFahrzeug() { if (SMAP && standortBekannt()) SMAP.flyTo([CAR.standortLat, CAR.standortLon], Math.max(SMAP.getZoom(), 15)); }
function standortAufUser() {
if (!USER_POS) { standortErfassen(); return; }
// Noch keine Position: die Verfolgung neu anstossen. Das ist zugleich der
// Weg zurueck, wenn die Freigabe erst nachtraeglich erteilt wurde - ein
// einmal gescheiterter watchPosition-Auftrag meldet sich nicht von selbst
// wieder.
if (!USER_POS) { standortVerfolgungBeenden(); standortVerfolgungStarten(); return; }
if (SMAP) SMAP.flyTo([USER_POS.lat, USER_POS.lon], Math.max(SMAP.getZoom(), 15));
}
function standortBeideZeigen() {
@@ -986,7 +1107,7 @@ async function initStandortMap() {
standortMarkerZeichnen();
standortBeideZeigen();
standortAdresseLaden();
standortErfassenFallsNoetig();
standortVerfolgungStarten();
setTimeout(() => SMAP && SMAP.invalidateSize(), 60);
}
async function initVorschauMap() {
@@ -2254,7 +2375,7 @@ function vIdent() {
<dl class="rows" style="margin-top:10px">
<div class="row"><dt>Modell</dt><dd>${esc(CONFIG.fahrzeugtitel)}</dd></div>
<div class="row"><dt>Ausführung</dt><dd>${CONFIG.ausfuehrung ? esc(CONFIG.ausfuehrung) : "noch offen"}</dd></div>
<div class="row"><dt>Fahrgestellnummer</dt><dd>${esc(CAR.fin)}</dd></div>
<div class="row"><dt>FIN</dt><dd>${esc(CAR.fin)}</dd></div>
<div class="row"><dt>Motor und Leistung</dt><dd>${esc(CAR.details)}</dd></div>
<div class="row"><dt>Erstzulassung</dt><dd>${esc(CAR.erstzulassung)}</dd></div>
<div class="row"><dt>Kennzeichen</dt><dd>${esc(CONFIG.kennzeichen)}</dd></div>
@@ -2763,7 +2884,7 @@ function vEinst() {
</div>
<div class="tile"><span class="label">Kraftstoff-Rabatt</span>
<div class="feld"><label for="sdAktiv" style="display:flex;align-items:center;gap:6px">
<img src="${STATIK}/bilder/${MARKENLOGO_DATEI}" alt="Shell" style="height:14px;width:auto" onerror="this.style.display='none'"> SmartDeal aktiv</label>
<img src="${STATIK}/bilder/${MARKENLOGO_DATEI}" alt="Shell" style="height:16px;width:auto" onerror="this.style.display='none'"> SmartDeal aktiv</label>
<label class="switch"><input id="sdAktiv" type="checkbox" data-sd ${CAR.smartdeal.aktiv ? "checked" : ""}><span class="track"></span></label></div>
${CAR.smartdeal.aktiv ? `
<div class="feld" style="border-bottom:0"><label>Gültig bis</label>
@@ -3897,7 +4018,10 @@ const ICONS = {
/* Wagen von vorn - dasselbe Symbol wie in der myAudi-App (Audi-Symbolsatz,
assets/icons/car.svg). Flaeche statt Kontur, deshalb die Klasse "voll". */
audi: '<path d="M5.46 4.05a80 80 0 0 1 13.08 0l.24.02.14.2A55 55 0 0 1 21.9 9.2l2.1 2.1v8.2c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V19l-14-.04v.54c0 .83-.67 1.5-1.5 1.5h-2A1.5 1.5 0 0 1 0 19.5v-8.2l2.1-2.1a55 55 0 0 1 2.98-4.93l.14-.2zM2.7 10 1 11.7v.3h4v1H1v6.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-1.55L20 18v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V13h-4v-1h4v-.3L21.3 10zm17.94-1a45 45 0 0 0-2.43-3.98 78 78 0 0 0-12.44 0L4.51 7c-.3.5-.7 1.16-1.16 2.01h17.3ZM7.3 12h9.42l1.36 1.36-.1.3c-.31 1-.6 1.78-.88 2.31-.13.27-.27.5-.41.67-.13.15-.36.36-.68.36H8a.9.9 0 0 1-.68-.36c-.14-.17-.28-.4-.41-.67a17 17 0 0 1-.89-2.32l-.09-.29zm.42 1-.64.64c.28.84.52 1.47.73 1.89a2.4 2.4 0 0 0 .3.47h7.8l.02-.01c.07-.08.16-.23.28-.46.21-.42.45-1.05.73-1.9L16.3 13H7.71Z"/>',
trips: '<circle cx="6" cy="6" r="2.2"/><circle cx="18" cy="18" r="2.2"/><path d="M8 7.5c4 1.5 4.5 3 3 5s-1 3.5 4.5 5"/>',
/* CI-Symbol tour-s statt der frueheren handgezeichneten Route - dieselbe
Fuellflaeche-statt-Kontur-Behandlung wie "audi" und "stat" (siehe
ICONS_VOLL). */
trips: '<path d="M3.4 3C2.6 3 2 3.64 2 4.5c0 .16.08.46.27.89a15.5 15.5 0 0 0 1.23 2.2 18.4 18.4 0 0 0 1.23-2.2c.19-.43.27-.73.27-.89C5 3.68 4.32 3 3.5 3zm.1 5.5-.41.28-.03-.03a18 18 0 0 1-.3-.45l-.7-1.14a14 14 0 0 1-.7-1.37A3.5 3.5 0 0 1 1 4.5C1 3.16 2 2 3.4 2h.1A2.5 2.5 0 0 1 6 4.5c0 .39-.17.86-.35 1.29a27 27 0 0 1-1.71 2.96l-.02.02v.01zm0 0 .41.28-.41.6-.41-.6zM12.4 3c-.8 0-1.4.64-1.4 1.5 0 .16.08.46.27.89a15.5 15.5 0 0 0 1.23 2.2 18.4 18.4 0 0 0 1.23-2.2c.19-.43.27-.73.27-.89 0-.82-.68-1.5-1.5-1.5zm.1 5.5-.41.28-.03-.03a12 12 0 0 1-.3-.45l-.7-1.14c-.25-.44-.5-.92-.7-1.37A3.5 3.5 0 0 1 10 4.5C10 3.16 11 2 12.4 2h.1A2.5 2.5 0 0 1 15 4.5c0 .39-.17.86-.35 1.29a27 27 0 0 1-1.71 2.96l-.02.02v.01zm0 0 .41.28-.41.6-.41-.6zm5.79-.5h.21a4.5 4.5 0 0 1 4.5 4.5c0 .66-.3 1.52-.67 2.38-.38.88-.9 1.83-1.4 2.7a54 54 0 0 1-1.97 3.14l-.04.05v.01l-.42-.28-.41.28-.01-.01-.04-.05a40 40 0 0 1-.58-.89 33 33 0 0 1-2.8-4.95A6.6 6.6 0 0 1 14 12.5 4.34 4.34 0 0 1 18.28 8Zm.21 12.5-.41.28.41.6.41-.6zm0-.9a53 53 0 0 0 1.57-2.53c.5-.85.98-1.76 1.35-2.6s.58-1.53.58-1.97c0-1.92-1.58-3.5-3.5-3.5h-.19c-1.94.1-3.4 1.58-3.31 3.48v.02c0 .44.2 1.13.58 1.98.37.83.86 1.74 1.35 2.6a53 53 0 0 0 1.57 2.52M9.94 11H3v-1h10.06l-7.61 5.44 13.29 7.12-.48.88-14.7-7.88L9.93 11Z"/>',
/* CI-Symbol polls-s statt der vorherigen handgezeichneten Balken - dieselbe
Fuellflaeche-statt-Kontur-Behandlung wie "audi" (siehe ICONS_VOLL). Anders
als die handgezeichneten Tab-Icons (die im 24er-Raster deutlich Rand
@@ -3920,7 +4044,7 @@ const TABS = [
{ id: "fuel", label: "Tanken", eyebrow: "", title: "Tanken" },
];
/* Symbole, die als Flaeche gezeichnet sind statt als Kontur */
const ICONS_VOLL = new Set(["audi", "stat"]);
const ICONS_VOLL = new Set(["audi", "stat", "trips"]);
let route = { name: "home" };
const ZURUECK = {
trip: "trips", fill: "fuel", service: "audi", reifen: "audi", ident: "audi",
@@ -4286,77 +4410,17 @@ const LUPE_SVG = `<svg viewBox="0 0 20 20" width="16" height="16" fill="none" st
/* Gemeinsames Präfix zweier domain-freier IDs, bis zum letzten gemeinsamen
"_" - oder null, wenn sie nicht einmal ein volles Segment teilen. */
function praefixPaar(a, b) {
let i = 0;
while (i < a.length && i < b.length && a[i].toLowerCase() === b[i].toLowerCase()) i++;
const idxUnter = a.slice(0, i).lastIndexOf("_");
return idxUnter > 0 ? a.slice(0, idxUnter + 1) : null;
}
/* Alle aktuell im Setup zugeordneten Entity-IDs (domain-frei), außer der
übergebenen selbst - Vergleichsbasis für entitaetIdKurz() unten. */
function setupAndereIdsOhneDomain(ausser) {
const ids = [];
for (const feld of setupKatalog()) {
const wert = setupZuordnung[feld.key];
if (feld.liste) { for (const w of (wert || [])) if (w && w !== ausser) ids.push(w); }
else if (wert && wert !== ausser) ids.push(wert);
}
return ids.map((id) => id.includes(".") ? id.split(".").slice(1).join(".") : id);
}
/* Die Entity-ID ohne Domain und ohne das installationsspezifische Präfix -
die tatsächliche, stabile Kennung des zugeordneten Sensors, unabhängig
davon, wie Geräte-Slug und Integrationsinstanz beim jeweiligen Nutzer
heißen (z. B. "testcar_b9_fmm003_testintegratoin_" oder
"audi_rs_4_avant_").
Das Präfix ist NICHT das mit dem einzelnen nächsten Verwandten geteilte
(frühere Fassung) - das kürzte in der Praxis zu viel: zwei Felder
derselben Sensorquelle können selbst wieder ein gemeinsames Namens-
Teilstück haben, das nichts mit der Installation zu tun hat (z. B. teilen
sich "oil_change_due" und "oil_change_distance" das Wortstück
"oil_change_", "can_fuel_volume" teilt sich "can_" mit einem anderen
CAN-Sensor) - genau das hat der Nutzer live beobachtet: "can_fuel_volume"
wurde zu bloß "fuel_volume", "oil_change_due" zu bloß "due". Stattdessen
zählt jedes Präfix, das eine Entität mit irgendeiner ANDEREN teilt, als
eine Stimme; das Präfix mit den MEISTEN Stimmen gewinnt. Das echte
Installationspräfix teilt sich die Entität mit fast allen anderen
Sensoren ihrer Quelle (viele Stimmen), ein zufällig geteiltes
Namens-Teilstück nur mit dem einen verwandten Feld (eine Stimme) - die
Mehrheit entscheidet also richtig, ohne einen einzelnen "nächsten"
Nachbarn zu bevorzugen. Bei Gleichstand gewinnt das kürzere Präfix
(vorsichtiger - weniger Kürzung im Zweifel). Mehrere gleichzeitige
Sensorquellen (z. B. FMM003 und eine zweite, "audi_rs_4_avant_"-
präfixte Integration) bleiben dabei weiterhin unabhängig voneinander
korrekt gekürzt, da die Stimmen pro Entität und nicht global gezählt
werden. Findet sich kein Verwandter mit gemeinsamem Präfix, bleibt die
volle domain-freie ID stehen statt einer geratenen, möglicherweise
falschen Kürzung. */
function entitaetIdKurz(entityId) {
if (!entityId) return null;
const ohneDomain = entityId.includes(".") ? entityId.split(".").slice(1).join(".") : entityId;
const stimmen = new Map();
for (const andere of setupAndereIdsOhneDomain(entityId)) {
const kandidat = praefixPaar(ohneDomain, andere);
if (!kandidat) continue;
stimmen.set(kandidat, (stimmen.get(kandidat) || 0) + 1);
}
let bestesPraefix = null, besteStimmen = 0;
for (const [kandidat, anzahl] of stimmen) {
const besser = anzahl > besteStimmen || (anzahl === besteStimmen && bestesPraefix && kandidat.length < bestesPraefix.length);
if (besser) { bestesPraefix = kandidat; besteStimmen = anzahl; }
}
return bestesPraefix ? ohneDomain.slice(bestesPraefix.length) : ohneDomain;
}
/* praefixPaar()/setupAndereIdsOhneDomain()/entitaetIdKurz() sind am 2026-08-31
entfallen: sie kürzten die Kennung des gerade zugeordneten Sensors für den
Beispieltext im Setup. Der kommt jetzt fest aus dem Feld-Katalog, damit er
sich nicht mehr mit jeder Auswahl ändert - siehe setupFeldzeile(). */
/* Die Art des erwarteten Werts, als kurzes Klammer-Etikett direkt neben der
Feld-Überschrift ("Tankfüllstand [%]", "12V-Batteriespannung [V]") - eine
feste Eigenschaft der FELD-ROLLE selbst (aus dem FELDER-Katalog,
einstellungen.py), unabhängig davon, ob und welcher Sensor gerade
zugeordnet ist. Bewusst NICHT der Live-Wert eines Sensors - der gehört
nirgends mehr hin, siehe entitaetIdKurz()'s eigenen Kommentar zur
Kopfzeile. binary_sensor liefert immer den Rohzustand "on"/"off"
nirgends mehr hin. binary_sensor liefert immer den Rohzustand "on"/"off"
(unabhängig von device_class - die beeinflusst nur, wie ANDERE
HA-Oberflächen ihn anzeigen, nicht den Rohwert, den diese App liest).
Bei mehreren möglichen Einheiten (z. B. "L"/"l") die klein geschriebene
@@ -4409,18 +4473,33 @@ function entitaetCombo(key, idx) {
}
function setupFeldzeile(feld) {
// Bei einem Listenfeld (vier Positionen wie Tür-/Fenstersensoren) gibt es
// keinen EINEN zugeordneten Sensor, dessen Kennung sich anzeigen ließe -
// jede Position hat ihre eigene Zuordnung, siehe die Unterfelder unten.
// Das Einheiten-Etikett neben dem Label gilt dagegen für die ganze Rolle
// und wird auch dort gezeigt.
// Einheit und Beispiel gelten beide für die ganze Feld-Rolle und stehen
// deshalb auch bei Listenfeldern (vier Positionen wie Tür-/Fenstersensoren).
const einheit = feldEinheitAnzeige(feld);
const erwartet = feld.liste ? "" : entitaetIdKurz(setupZuordnung[feld.key]);
// Nutzerkorrektur 2026-08-28: kein erfundenes statisches Beispiel mehr (war
// für Reichweite/Sofort-Aktualisierung schlicht falsch) - nur EINE Zeile,
// der tatsächlich zugeordnete Sensor (erwartet), als "(z. B. ...)"
// dargestellt. Der Klammertext selbst in Label-Größe/-Farbe, der Sensorname
// darin in derselben kleinen/grauen Schrift wie die Einheit ("[on/off]").
/* Das Beispiel steht fest im Feld-Katalog (FELDER in einstellungen.py,
Schlüssel "beispiel") und gehört damit zur FELD-ROLLE - genau wie das
Einheiten-Etikett daneben.
Vorgeschichte in zwei Schritten:
Bis 2026-08-28 stand hier ein von Hand ausgedachtes Beispiel. Das war
für Reichweite und Sofort-Aktualisierung schlicht falsch, und der
Nutzer hat es zu Recht beanstandet. Der Ersatz - die gekürzte Kennung
des GERADE ZUGEORDNETEN Sensors - war zwar nie falsch, aber auch kein
Beispiel mehr: er änderte sich mit jeder Auswahl mit und sagte einem
damit nur zurück, was man eben selbst eingestellt hatte (Nutzerkorrektur
2026-08-31: "Ich möchte, dass xxx permanent bleibt").
Jetzt wieder fest - aber nicht ausgedacht: die Namen sind die der
tatsächlich laufenden Integration, abgelesen aus der Zuordnung dieser
Installation. Damit ist das Beispiel stabil UND wahr, und beide
Beanstandungen sind zugleich erfüllt.
Listenfelder bekommen es ebenfalls: dort ist der Name der ersten
Position das Muster, nach dem sich die anderen drei richten. */
const erwartet = feld.beispiel || "";
// Der Klammertext in Label-Größe/-Farbe, der Sensorname darin in derselben
// kleinen/grauen Schrift wie die Einheit ("[on/off]").
const hinweis = erwartet
? `<span class="setup-feld-hinweis">(z. B. <span class="setup-feld-einheit">${esc(erwartet)}</span>)</span>`
: "";
@@ -4565,6 +4644,7 @@ function render() {
Aenderungspruefung in datenLaden(). */
if (MAP) { MAP.remove(); MAP = null; TILE = null; }
if (route.name === "trip" || route.name === "fill") initMap().catch(() => {}); // ohne Internet bleibt die Kartenflaeche leer
standortVerfolgungBeenden();
if (SMAP) { SMAP.remove(); SMAP = null; SMAP_TILE = null; FAHRZEUG_MARKER = null; USER_MARKER = null; }
if (route.name === "standort") initStandortMap().catch(() => {});
if (TMAP) { TMAP.remove(); TMAP = null; TMAP_TILE = null; }
@@ -5953,6 +6033,7 @@ class AudiDashboardPanel extends HTMLElement {
// (Standort- oder Uebersichtsseite offen) eine Leaflet-Instanz samt ihrem
// window-Resize-Horcher und laufenden Kachelabrufen am Leben.
if (MAP) { MAP.remove(); MAP = null; TILE = null; }
standortVerfolgungBeenden();
if (SMAP) { SMAP.remove(); SMAP = null; SMAP_TILE = null; FAHRZEUG_MARKER = null; USER_MARKER = null; }
if (TMAP) { TMAP.remove(); TMAP = null; TMAP_TILE = null; }
}
@@ -5992,6 +6073,7 @@ class AudiDashboardPanel extends HTMLElement {
<button class="back" id="back" aria-label="Zurück"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="m10.21 12 5.12-5.08-.7-.71L8.79 12l5.84 5.8.7-.72L10.22 12Z"/></svg></button> <button type="button" class="rings" id="marke" aria-label="Einstellungen"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 289 100" fill="currentColor" aria-hidden="true"><defs><clipPath id="audiRingsClip" clipPathUnits="userSpaceOnUse"><path d="M0 100h289V0H0z"/></clipPath></defs><g clip-path="url(#audiRingsClip)" transform="matrix(1 0 0 -1 0 100.00040017)"><path d="M239 11.1914c-8.885 0-17.075 3.008-23.624 8.05 6.646 8.489 10.624 19.165 10.624 30.759 0 11.593-3.978 22.269-10.624 30.759 6.549 5.041 14.739 8.05 23.624 8.05 21.399 0 38.809-17.41 38.809-38.809 0-21.4-17.41-38.809-38.809-38.809m-86.625 8.05c6.647 8.489 10.625 19.165 10.625 30.759 0 11.593-3.978 22.269-10.624 30.759 6.548 5.041 14.74 8.05 23.624 8.05 8.885 0 17.075-3.009 23.624-8.05-6.646-8.49-10.624-19.166-10.624-30.759 0-11.594 3.978-22.27 10.624-30.759-6.549-5.042-14.739-8.05-23.624-8.05-8.884 0-17.076 3.008-23.625 8.05m-63 0C96.022 27.7304 100 38.4064 100 50.0004c0 11.593-3.978 22.269-10.624 30.759 6.548 5.041 14.74 8.05 23.624 8.05 8.884 0 17.076-3.009 23.624-8.05-6.646-8.49-10.624-19.166-10.624-30.759 0-11.594 3.978-22.27 10.624-30.759-6.548-5.042-14.74-8.05-23.624-8.05-8.884 0-17.076 3.008-23.625 8.05m-78.184 30.759c0 21.399 17.41 38.809 38.809 38.809 8.884 0 17.076-3.009 23.624-8.05C66.978 72.2694 63 61.5934 63 50.0004c0-11.594 3.978-22.27 10.624-30.759-6.548-5.042-14.74-8.05-23.624-8.05-21.399 0-38.809 17.409-38.809 38.809M81.5 27.3734c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m63 0c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m63 0c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m31.5 72.627c-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.568 11.207-31.5 11.207-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.569 11.207-31.5 11.207-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.569 11.207-31.5 11.207-27.57 0-50-22.43-50-50 0-27.571 22.43-50 50-50 11.931 0 22.896 4.206 31.5 11.207C90.104 4.2064 101.068.0004 113 .0004c11.931 0 22.896 4.206 31.5 11.207 8.604-7.001 19.568-11.207 31.5-11.207 11.932 0 22.896 4.206 31.5 11.207 8.604-7.001 19.568-11.207 31.5-11.207 27.57 0 50 22.429 50 50 0 27.57-22.43 50-50 50"/></g></svg></button>
<div class="head"><div class="eyebrow" id="eyebrow"></div><div class="title" id="title">Übersicht</div>
<div class="sync" id="sync"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="m18.65 4.95 2.56 2.55-2.56 2.55-.7-.7L19.29 8H7.2A5.24 5.24 0 0 0 2 13.38V14H1v-.6A6.24 6.24 0 0 1 7.2 7h12.1l-1.34-1.35.7-.7ZM23 11.59A6.24 6.24 0 0 1 16.8 18H4.7l1.35 1.35-.7.7-2.56-2.55 2.56-2.55.7.7L4.71 17H16.8a5.24 5.24 0 0 0 5.2-5.38V11h1v.6Z"/></svg><span></span></div></div>
<span class="kopfgegengewicht" aria-hidden="true"></span>
<button class="themebtn" id="theme" hidden aria-hidden="true"><svg id="themeicon" viewBox="0 0 20 20" width="15" height="15"></svg></button>
<button class="profilbtn" id="ringsbtn" aria-label="Einstellungen"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.6 1.6 0 0 0 .33 1.76l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.6 1.6 0 0 0-1.76-.33 1.6 1.6 0 0 0-1 1.47V21a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-1-1.47 1.6 1.6 0 0 0-1.77.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.6 1.6 0 0 0 4.6 15a1.6 1.6 0 0 0-1.47-1H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.47-1 1.6 1.6 0 0 0-.33-1.77l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.6 1.6 0 0 0 9 4.6 1.6 1.6 0 0 0 10 3.13V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 1 1.47 1.6 1.6 0 0 0 1.77-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.6 1.6 0 0 0 19.4 9v.04a1.6 1.6 0 0 0 1.47 1H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z"/></svg></button>
</div>