Kalender ueber EventKit, Orte nach dem Import, Fotos verkleinert, Signalsperre (2026.9.3.1-.3)
- Kalendertermine gehen nativ ueber EventKit statt ueber das Teilen-Blatt:
Apples Kalender meldet sich beim System gar nicht als Teilen-Ziel an, ein
anderes Dateiformat haette also nie geholfen (Abschnitt BS).
- Der Rueckblick stoesst jetzt selbst das Screening an, wenn er Fahrten
angelegt hat - sonst blieb eine importierte Fahrt ohne Ort liegen, solange
das Fahrzeug steht (Abschnitt BT).
- Fahrzeugfotos werden vor dem Upload im Browser auf 2000 px/WebP gebracht.
Gemessen: 3.099.022 -> 325.994 Bytes. Damit ist die Nachrichtengrenze der
WebSocket-Verbindung kein Thema mehr ("connection lost" auf der realen
Instanz). Formate, die der Browser weder umwandeln noch anzeigen kann,
werden mit klarer Meldung abgelehnt statt roh gespeichert.
- Zwei Wechsel des Fahrtsignals in derselben Sekunde kosteten eine ganze
Fahrt: das "on" ueberholte den noch laufenden Beende-Vorgang und fiel durch
beide Zweige. Neue asyncio-Sperre plus Regressionstest, der ohne sie
nachweislich rot ist (Abschnitt BU).
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||
|
||||
**Last updated: 2026-09-02** (Null heisst unbekannt, Momentanwerte nur fuer
|
||||
**Last updated: 2026-09-03** (Zwei Signalwechsel in derselben Sekunde kosteten eine Fahrt - Sperre
|
||||
plus Regressionstest, Bildformat-Pruefung, `2026.9.3.3`, Abschnitt BU. Davor: Rueckblick loest Orte
|
||||
selbst auf, Fotos werden vor dem Upload verkleinert, Abschnitt BT. Davor: Kalendertermine gehen ueber EventKit statt ueber das
|
||||
Teilen-Blatt, Abschnitt BS. Davor: Null heisst unbekannt, Momentanwerte nur fuer
|
||||
"jetzt", Uebersicht-Symbol vergroessert, `2026.9.2.7`, Abschnitte BN und BO.
|
||||
Davor: Verbrauchsfaktor aus dem Tankbeleg, `2026.9.1.32`,
|
||||
Abschnitt BM. Davor: Reifenzaehler ohne Fahrzeugwechsel, „Raeder“
|
||||
@@ -8320,9 +8323,9 @@ Zwei verschiedene Termine überschrieben einander damit im Zwischenspeicher. Uml
|
||||
umgeschrieben statt weggeworfen (ein „ö" im Dateinamen muss über die Teilen-Kette prozentkodiert
|
||||
werden — eine Fehlerquelle weniger), und die Übergabe läuft über `files` statt `url`.
|
||||
|
||||
**Nicht bestätigt ist, dass das den Kalender-Export repariert.** Der Verdacht bleibt, dass iOS
|
||||
„Kalender" im Teilen-Blatt gar nicht als Ziel führt; der verlässliche Weg wäre EventKit, also
|
||||
nativ. Steht zur Prüfung beim Eigentümer.
|
||||
**Der Dateiname war nicht die Ursache.** Der Verdacht daneben — iOS führt „Kalender" im
|
||||
Teilen-Blatt gar nicht als Ziel — hat sich am 03.09. bestätigt; gebaut ist jetzt der EventKit-Weg,
|
||||
siehe Abschnitt BS.
|
||||
|
||||
### Was am 02.09. abends offen blieb
|
||||
|
||||
@@ -8413,3 +8416,277 @@ weil `ios/` jederzeit neu erzeugt werden kann.
|
||||
Alles Native: der Umbau der Übergabe und das Zwischenablage-Plugin. Die drei Teile davor
|
||||
(Backend-Feld, Warteschleife, Anzeige) laufen per OTA. **Rein per OTA ist die Brücke über die
|
||||
App-Gruppe nicht zu reparieren** — egal welchen Weg man nimmt.
|
||||
|
||||
---
|
||||
|
||||
## BS. Der Kalender kommt über EventKit, weil das Teilen-Blatt ihn nie anbieten kann (2026.9.3.1)
|
||||
|
||||
### Der Befund: es lag nie am Dateinamen und nie am Format
|
||||
|
||||
Gemeldet am 02.09.2026, nachdem der `-.ics`-Fehler (Abschnitt BQ) behoben war und es trotzdem nicht
|
||||
half: „iOS bietet keinen Kalender an. Schicke ich mir die ics selbst per Mail zu, kann ich diese
|
||||
anklicken und der Kalender öffnet sich."
|
||||
|
||||
Recherchiert statt geraten, und die Antwort ist eindeutig: **Apples Kalender-App meldet sich beim
|
||||
System überhaupt nicht als Teilen-Ziel an** — für kein Format. Das Teilen-Blatt zeigt nur Apps mit
|
||||
einer Share-Erweiterung; damit kann keine App der Welt dort einen Kalender erscheinen lassen. Ein
|
||||
anderes Format hilft nicht: `.ics` (iCalendar, RFC 5545) ist das einzige, das iOS für Termine kennt.
|
||||
|
||||
Der funktionierende Mail-Weg führte in die Irre. Ein angetippter Anhang öffnet **nicht** das
|
||||
Teilen-Blatt, sondern die Dokumentvorschau des Systems (Quick Look), die `text/calendar` erkennt und
|
||||
„Alle hinzufügen" einblendet — ein anderer Mechanismus, den eine App aus dem Teilen-Blatt heraus
|
||||
nicht erreicht. Belege: [Apple Community](https://discussions.apple.com/thread/255910569),
|
||||
[HomeBase Software](https://hbase.net/2021/07/31/adding-ics-files-to-calendar-on-ios/) (beide
|
||||
beschreiben denselben Fall und weichen auf Kurzbefehle aus).
|
||||
|
||||
Drei mögliche Wege standen zur Wahl: EventKit, Quick Look auf die eigene `.ics` (bildet den
|
||||
Mail-Weg nach), oder `webcal://` — letzteres scheidet aus, weil es ein **Abonnement** ergibt
|
||||
(schreibgeschützt, wird abgefragt) und nicht einen Termin. Gewählt: EventKit.
|
||||
|
||||
### `native/KalenderTermin.swift`
|
||||
|
||||
Ein eigenes kleines Plugin nach dem Muster von `BelegZwischenablage.swift`, kein Fremdpaket
|
||||
(`@ebarooni/capacitor-calendar` 8.5.0 wäre kompatibel gewesen, bringt aber Android-Code und
|
||||
Erinnerungen mit, die hier beide niemand braucht).
|
||||
|
||||
Zwei Entscheidungen darin, die nicht offensichtlich sind:
|
||||
|
||||
* **`EKEventEditViewController` statt `EKEventStore.save`.** Das passt zur Vorgabe des Eigentümers,
|
||||
dass der Werkstatttermin keine eigene Verwaltung bekommt (Abschnitt BQ): die App liefert „was,
|
||||
wann, welche Werkstatt" und ist danach raus — bestätigt und gespeichert wird im Kalender. Der
|
||||
zweite Grund ist die Berechtigung: seit iOS 17 braucht der System-Editor keine (WWDC23, „Discover
|
||||
Calendar and EventKit"), ein direktes `save()` dagegen schon.
|
||||
* **`event.calendar` wird bewusst NICHT gesetzt.** Das würde `defaultCalendarForNewEvents` lesen und
|
||||
damit doch eine Freigabe verlangen; der Editor sucht den Standardkalender selbst.
|
||||
|
||||
Dazu zwei Kleinigkeiten mit Grund: `isModalInPresentation = true`, weil ein Wegwischen den
|
||||
Delegaten nicht ruft und der wartende Aufruf sonst für immer hinge — und ein noch offener Aufruf
|
||||
wird beim nächsten Öffnen als „abgebrochen" aufgelöst, statt ein Versprechen nie einzulösen.
|
||||
Ganztägig mit Erinnerung zwei Tage vorher, also dasselbe wie `DTSTART;VALUE=DATE` und
|
||||
`TRIGGER:-P2D` der bisherigen Datei.
|
||||
|
||||
### Der Rückfall gilt nur für eine alte .ipa
|
||||
|
||||
`terminUebernehmen()` (früher `kalenderDateiLaden()` — der alte Name beschrieb den Weg, den es nicht
|
||||
mehr gibt) fällt auf Datei plus Teilen-Blatt **ausschließlich** bei `code === "UNIMPLEMENTED"`
|
||||
zurück, also wenn die installierte App das Plugin nicht kennt. Jeder andere Fehler wird
|
||||
durchgereicht und in beiden Aufrufstellen als `.dm-fehler` angezeigt.
|
||||
|
||||
Der Unterschied ist der Punkt: würde jeder Fehler in den Rückfall laufen, verschwände ein echtes
|
||||
Problem hinter einem Teilen-Blatt, aus dem der Termin nie in den Kalender fände — genau die
|
||||
Sackgasse, die dieser Abschnitt beseitigt.
|
||||
|
||||
### Panel unverändert, mit Begründung
|
||||
|
||||
Das Panel läuft im Browser, dort lädt die `.ics` ganz normal herunter und das Betriebssystem
|
||||
übernimmt sie wie jede andere Datei. EventKit gibt es dort nicht. Kein Portierungsbedarf nach der
|
||||
Paritätsregel — eine echte Ausnahme, keine stillschweigend ausgelassene Arbeit.
|
||||
|
||||
### Zwei Info.plist-Schlüssel, obwohl der Editor nicht fragt
|
||||
|
||||
`ios-signieren.sh` setzt `NSCalendarsWriteOnlyAccessUsageDescription` (iOS 17+) **und**
|
||||
`NSCalendarsUsageDescription` (älter), aus demselben Grund wie den Standort-Schlüssel: `ios/` ist
|
||||
gitignored und wird von `npx cap add ios` neu erzeugt. Sie sind die Absicherung für den Fall, dass
|
||||
EventKit doch einmal fragt — fehlt der passende, beendet iOS die App wortlos, statt eine Rückfrage
|
||||
zu zeigen.
|
||||
|
||||
`ios-teilen-einrichten.mjs` führt die eigenen Plugins jetzt als Liste (`PLUGIN_DATEIEN`) statt als
|
||||
Einzelfall; Kopieren und Anhängen an die Sources-Phase des App-Ziels laufen darüber.
|
||||
|
||||
### Verifiziert — und was ausdrücklich nicht
|
||||
|
||||
`tsc --noEmit` sauber, **173/173** Tests grün (vier neue in `src/screens/kalender.test.ts`:
|
||||
natives Gerät nimmt das Plugin und nicht das Teilen-Blatt, Abbruch ist kein Fehler, fehlendes
|
||||
Plugin fällt auf die Datei mit korrektem Namen zurück, echter Fehler wird durchgereicht),
|
||||
`vite build` sauber, `node --check` auf dem Einrichtungsskript, `bash -n` auf dem Signierskript,
|
||||
Manifest und Bündel beide auf `2026.9.3.1`.
|
||||
|
||||
**Nicht geprüft, weil es ohne Mac nicht geht:** dass der Swift-Code kompiliert und der Editor auf
|
||||
dem Gerät erscheint. Abschnitt AK ist die Warnung dazu — dort haben vier von fünf Fehlern einen
|
||||
erfolgreichen Archivlauf erzeugt und sind erst am fertigen `.ipa` aufgefallen. Beim nächsten
|
||||
`ios-signieren.sh` gehört deshalb geprüft: `KalenderTermin.swift` in der Sources-Phase des
|
||||
App-Ziels, beide Kalender-Schlüssel im Info.plist des Bündels.
|
||||
|
||||
---
|
||||
|
||||
## BT. Der Rückblick löst jetzt selbst Orte auf; Fotos werden vor dem Upload verkleinert (2026.9.3.2)
|
||||
|
||||
### Die importierte Fahrt blieb ohne Ort, obwohl beide Koordinaten dastanden
|
||||
|
||||
Gemeldet am 03.09.2026 an der importierten Fahrt vom 02.09. (`t-b2a9170e7403`, 21:22–21:58,
|
||||
14,0 km). Nachgesehen statt vermutet: `start_lat`/`end_lat` standen da, und **beide Schlüssel lagen
|
||||
sogar schon im Zwischenspeicher** (`orte.json`: `48.887,11.197` → Eichstätt, `48.842,11.225` →
|
||||
Adelschlag). Es fehlte also weder Netz noch Nominatim — es fehlte der Auslöser.
|
||||
|
||||
`historienimport.importieren()` rief das Screening nie. Die einzigen Auslöser waren ein Wechsel des
|
||||
Kilometerstands, ein Neustart und das Nachfassen. Steht das Fahrzeug — nach einem Import über
|
||||
Vergangenes der Normalfall —, ist keiner davon der Fall, und die frische Fahrt bleibt liegen. Im
|
||||
Protokoll ist es exakt so zu sehen: `Import abgeschlossen … fahrten_angelegt: 1` um 23:12:23, danach
|
||||
keine einzige Screening-Zeile mehr.
|
||||
|
||||
**Dass es so gemeint war, sagt der Code selbst:** der Docstring von `_nachfassen_planen()` nennt
|
||||
„nach einem Rückblick-Import aus Home Assistant" als einen der zwei Fälle, für die das Nachfassen
|
||||
überhaupt existiert. Ausgelöst hat den Import-Fall nur nie jemand.
|
||||
|
||||
Jetzt: `if fahrten["angelegt"]: await screening.durchfuehren(k)` am Ende von `importieren()`. Nur
|
||||
bei wirklich angelegten Fahrten — ein zweiter Lauf über denselben Zeitraum legt nichts an und soll
|
||||
keinen Durchlauf kosten. `from . import screening` ist auf Modulebene unbedenklich: `screening`
|
||||
zieht `reifen`, `verbrauchskorrektur` und `verlauf`, keines davon `historienimport` (geprüft, nicht
|
||||
angenommen) — und `dienste.py` lädt `historienimport` beim Setup, ein Zyklus wäre also sofort als
|
||||
Startfehler sichtbar.
|
||||
|
||||
Damit trägt der Import auch das nach, was sonst am Screening hängt und ihm bisher entging:
|
||||
Verbrauchseichung, Radzähler, Durchschnittsgeschwindigkeit auf nachträglich verfeinerten Strecken.
|
||||
|
||||
**Verifiziert:** ein Neustart (dessen `_nach_start()`-Screening) füllte beide Orte binnen Sekunden
|
||||
aus dem Zwischenspeicher — der Beweis, dass an der Fahrt selbst nie etwas fehlte. Der Import läuft
|
||||
mit der Änderung sauber durch (`fahrten_angelegt: 0` über denselben Zeitraum, kein Traceback,
|
||||
Screening korrekt **nicht** ausgelöst). **Nicht live gezeigt:** der Zweig selbst bei
|
||||
`angelegt > 0` — dafür hätte die importierte Fahrt gelöscht und neu importiert werden müssen, und
|
||||
das Löschen echter Fahrtdaten wurde abgelehnt. Alles, was der Zweig aufruft, ist dagegen bewiesen.
|
||||
|
||||
### Die beiden Fahrten, die zu prüfen waren
|
||||
|
||||
| | `t-0591dace4194` | `t-b2a9170e7403` |
|
||||
|---|---|---|
|
||||
| Zeitraum | 02.09. 16:32–21:05 (4 h 34) | 02.09. 21:22–21:58 (36 Min.) |
|
||||
| Herkunft | live erkannt | Import |
|
||||
| Strecke | **keine** | 14,0 km (209490 → 209504) |
|
||||
| Orte | Adelschlag → Eichstätt | Eichstätt → Adelschlag (jetzt) |
|
||||
|
||||
Die lange Fahrt ist **nicht reparierbar, und der Code hat richtig gehandelt**: sie läuft über einen
|
||||
Fahrzeugwechsel des Dongles. `odo_start` 61823 (RS4), am Ende meldete der Zähler 209490 — das andere
|
||||
Auto. Die Plausibilitätsprüfung verwarf beide Werte („unmögliche 32381,1 km/h im Schnitt"), statt
|
||||
147.667 km festzuschreiben. Dass sie 4 h 34 dauert, ist die schon bekannte Lage: das Trip-Signal
|
||||
meldete ihr Ende nie (siehe „Was am 02.09. abends offen blieb").
|
||||
|
||||
Eine Strecke ließe sich aus dem GNSS-Zähler holen — die Route mit 111 Punkten liegt vor —, aber
|
||||
`_gnss_verfeinern()` braucht den Kilometerstand als Anker und läuft deshalb nur auf Fahrten, die
|
||||
schon eine grobe Strecke haben. Der Rückfall „ohne Anker allein aus GNSS" ist der Vorschlag vom
|
||||
02.09., den der Eigentümer bis zur Dongle-Prüfung zurückgestellt hat. Unverändert offen.
|
||||
|
||||
Die importierte Fahrt ist in sich stimmig: Zählerdifferenz 14 km bei 36 Minuten, Route Eichstätt →
|
||||
Adelschlag, also die Rückfahrt. Die absoluten Zählerstände stammen zwar aus dem anderen Fahrzeug,
|
||||
die Differenz aber aus einer einzigen Fahrt — deshalb greift die Prüfung zu Recht nicht.
|
||||
|
||||
### „Hochladen fehlgeschlagen … connection lost" — und warum es in der Testinstanz ging
|
||||
|
||||
Der Upload läuft als Dienstaufruf über die **WebSocket-Verbindung**, und die hat eine Obergrenze je
|
||||
Nachricht: aiohttps Vorgabe von 4 MiB, wobei Base64 die Datei vorher um ein Drittel aufbläht — die
|
||||
Decke liegt also bei rund 3,0 MB Rohbild (in dieser Sitzung gemessen). Über die reale Instanz kommen
|
||||
Cloudflare und der Vorschaltserver dazu, die enger sein können. Genau daher der Unterschied: dasselbe
|
||||
Foto ging über localhost durch und riss über die reale Instanz die Verbindung ab. „connection lost"
|
||||
ist deshalb auch keine Meldung des Backends — dort kam nie etwas an.
|
||||
|
||||
Statt die Grenze zu verschieben (ein eigener HTTP-Endpunkt mit HAs 16-MiB-Decke wäre der andere Weg,
|
||||
er steht weiter als offener Punkt in Abschnitt BQ) wird das Foto jetzt **vor dem Senden im Browser
|
||||
verkleinert**: längste Kante 2000 px, WebP bei 0,85. Die größte Fläche, auf der ein Foto je
|
||||
erscheint, ist die Inhaltsspalte im Breitbildlayout mit 772 px — 2000 px lassen dafür reichlich Luft.
|
||||
|
||||
Drei Rückfälle, alle auf das Original: Format nicht dekodierbar, kein WebP-Encoder, oder das
|
||||
Ergebnis ist **größer** als das Original (ein bereits sparsam gespeichertes Foto soll nicht ein
|
||||
zweites Mal verlustbehaftet kodiert werden). Schlechter als vorher kann es dadurch nie werden.
|
||||
|
||||
Nebeneffekt, der eine alte Unsauberkeit mitnimmt: das Backend schreibt die Bytes unverändert unter
|
||||
einen `.webp`-Namen (`ERLAUBTE_DATEINAMEN` in `bilder.py`). Bisher lag dort also je nach Quelle ein
|
||||
JPEG oder PNG mit falscher Endung; jetzt ist es wirklich WebP.
|
||||
|
||||
**Live am laufenden Panel nachgewiesen**, nicht gerechnet: ein fotoähnliches 4000×2250-JPEG von
|
||||
**3.099.022 Bytes** (base64 4.132.032 — schon dicht an der 4-MiB-Decke) durch den echten
|
||||
`data-bildupload`-Weg geschickt; auf der Platte landeten **325.994 Bytes** als 2000×1125-WebP, ein
|
||||
Faktor von 9,5. Das Testbild wurde danach über `bild_loeschen` wieder entfernt, der Platz war vorher
|
||||
leer.
|
||||
|
||||
Verifiziert: `tsc --noEmit` sauber, 173/173 Tests, Panel als **Modul** geparst (Abschnitt AQ),
|
||||
`audi_ha_test` auf `2026.9.3.2` sauber gestartet, kein Traceback, Bündel und Manifest gleichauf
|
||||
(sha256 `09c67475…`).
|
||||
|
||||
---
|
||||
|
||||
## BU. Zwei Signalwechsel in derselben Sekunde kosteten eine ganze Fahrt (2026.9.3.3)
|
||||
|
||||
### Die Frage, die es aufgedeckt hat
|
||||
|
||||
„Warum ist die Fahrt nicht von alleine erschienen und musste importiert werden?" — die richtige
|
||||
Frage, und die Antwort steht im Verlauf, nicht in einer Vermutung:
|
||||
|
||||
| Ankunft in HA | Trip-Signal | Zündung |
|
||||
|---|---|---|
|
||||
| 21:22:12 | **off** | on |
|
||||
| 21:22:12 | **on** | |
|
||||
| 21:22:37 | | off, dann on |
|
||||
| 21:43:15 | | off |
|
||||
| 21:58:16 | off | |
|
||||
|
||||
Das Gerät hatte gepuffert: der Datensatz „Fahrt zu Ende" (Gerätezeit 21:20:55) und der Datensatz
|
||||
„Fahrt begonnen" kamen **in derselben Sekunde** an. Das Gerät hat sauber geliefert — der Fehler lag
|
||||
bei uns.
|
||||
|
||||
### Der Mechanismus
|
||||
|
||||
Home Assistant startet für jeden Zustandswechsel eine **eigene Aufgabe**
|
||||
(`_fahrtsignal_geaendert` ist eine Koroutine). `zuendung_geaendert()` entscheidet an
|
||||
`fahrt_start_ts`, ob eine Fahrt läuft — und der Beende-Zweig wartet danach mehrfach:
|
||||
`_geraetezeit()` bis zu zwei Sekunden auf die Meldezeit, `_echtes_ende()` liest den Verlauf,
|
||||
`fahrt_beenden()` schreibt und stösst das Screening an. Erst ganz am Ende wurde `fahrt_start_ts`
|
||||
zurückgesetzt.
|
||||
|
||||
Das `on` traf mitten in diese Wartezeit, sah `fahrt_start_ts` noch gesetzt — also „es läuft eine
|
||||
Fahrt" — und passte damit in **keinen** der beiden Zweige: weder „an und es läuft keine" noch „aus
|
||||
und es läuft eine". Es fiel wortlos durch. Danach räumte der Beende-Vorgang das Feld ab, und das
|
||||
reguläre `off` um 21:58:16 fand keine laufende Fahrt mehr.
|
||||
|
||||
36 Minuten Fahrt, live nie erfasst. Gefunden hat sie nur der Rückblick, weil der den Verlauf als
|
||||
Ganzes liest, statt auf Ereignisse zu reagieren.
|
||||
|
||||
### Die Sperre, und warum nicht früheres Zurücksetzen
|
||||
|
||||
`k.fahrt_sperre` (`asyncio.Lock`, im Koordinator) umschliesst den Rumpf; der Rumpf selbst steckt
|
||||
jetzt in `_signalwechsel()`. Entscheidend ist, dass `fahrt_laeuft` **innerhalb** der Sperre gelesen
|
||||
wird: ein Wechsel, der auf seinen Vorgänger gewartet hat, muss dessen Ergebnis sehen.
|
||||
`nach_neustart_fortsetzen()` nimmt dieselbe Sperre — der erste Signalwechsel kann eintreffen,
|
||||
während der Nachlauf noch schreibt.
|
||||
|
||||
Die naheliegende Alternative — `fahrt_start_ts` sofort löschen, statt am Ende — wurde **verworfen**:
|
||||
fällt der Beende-Vorgang unterwegs aus, bliebe der Zwischenstand dann gelöscht und die Fahrt wäre
|
||||
verloren. Mit der Sperre bleibt er stehen wie bisher, und der nächste Auslöser schliesst sie nach.
|
||||
|
||||
### Der Test beweist es, statt es zu behaupten
|
||||
|
||||
`tests/fahrterkennung/test_signalwechsel.py` (5 Fälle, `IsolatedAsyncioTestCase`) stellt genau die
|
||||
Verschränkung nach: die langsamen Teile sind ersetzt, `_geraetezeit` wartet messbar, zwei Aufgaben
|
||||
laufen gegeneinander. **Gegen den alten Stand nachgewiesen fehlschlagend** — die Sperrzeile im
|
||||
Container durch `if True:` ersetzt, und der Test meldet exakt das gemeldete Symptom:
|
||||
`AssertionError: unexpectedly None : nach dem Paar muss die neue Fahrt laufen`. Danach zurückgesetzt,
|
||||
5/5 grün.
|
||||
|
||||
Merkposten daraus, allgemeiner als dieser eine Fall: **jeder Zustandsbeobachter in dieser
|
||||
Integration, der awaitet und dabei eigenen Zwischenstand fortschreibt, kann von seinem eigenen
|
||||
Nachfolger überholt werden.** Home Assistant serialisiert die Rückrufe nicht.
|
||||
|
||||
### Bildformate: was der Browser nicht umwandeln kann, wird jetzt abgelehnt
|
||||
|
||||
Auf die Frage nach den erlaubten Formaten nachgesehen: `accept="image/*"` in beiden Oberflächen,
|
||||
und `bilder.py` prüft am Inhalt **nichts** — es dekodiert base64 und schreibt die Bytes unter einen
|
||||
der elf festen `.webp`-Namen. Bis zur Verkleinerung (Abschnitt BT) lag dort also je nach Quelle ein
|
||||
JPEG mit falscher Endung; aufgefallen ist das nie, weil Browser den Typ am Inhalt erkennen.
|
||||
|
||||
Der Randfall, der dadurch entstand: HEIC. Über die iPhone-Fotoauswahl kommt in der Regel JPEG an,
|
||||
am Mac im Finder gewählt aber eine `.heic` — und die kann ausser Safari/WKWebView niemand
|
||||
dekodieren. Der Rückfall auf das Original hätte sie dann roh gespeichert, und das Panel zeigte
|
||||
hinterher ein leeres Bild ohne jede Erklärung.
|
||||
|
||||
`bildformatIstSicher()` (wortgleich in beiden Codebasen) prüft JPEG/PNG/WebP/GIF/BMP/AVIF am
|
||||
MIME-Typ, bei leerem Typ an der Endung — dieselbe Doppelprüfung wie `istPdf()`. Ist die Umwandlung
|
||||
gescheitert **und** das Original nicht anzeigbar, wird der Upload mit klarer Meldung abgelehnt statt
|
||||
etwas zu speichern, das niemand darstellen kann. Ein anzeigbares Original geht unverändert durch.
|
||||
|
||||
Nebenbei geschlossen: in der App war ein Fehlschlag beim Hochladen bisher **unsichtbar** — der
|
||||
Aufruf lief als `void hochladen(f)` ins Leere. Jetzt steht die Meldung unter dem Bild.
|
||||
|
||||
Live am laufenden Panel geprüft: eine vorgetäuschte `IMG_4711.heic` erzeugt
|
||||
„Dieses Bildformat kann der Browser nicht umwandeln (image/heic). Bitte das Foto als JPEG, PNG oder
|
||||
WebP sichern und noch einmal versuchen." — und auf der Platte entsteht **nichts**.
|
||||
|
||||
Verifiziert: 5/5 Backend-Tests (und ohne die Sperre nachweislich rot), `tsc --noEmit` sauber,
|
||||
173/173 App-Tests, Panel als Modul geparst, `audi_ha_test` auf `2026.9.3.3` sauber gestartet.
|
||||
|
||||
Reference in New Issue
Block a user