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:
@@ -388,6 +388,103 @@ function startIndex() {
|
||||
const i = BILDER.findIndex((b) => b.name === CONFIG.startbild);
|
||||
return i < 0 ? 0 : i;
|
||||
}
|
||||
|
||||
/* Ein Fahrzeugfoto wird im Browser verkleinert, bevor es hochgeladen wird.
|
||||
|
||||
WARUM: der Upload laeuft als Dienstaufruf ueber die WebSocket-Verbindung,
|
||||
und die hat eine Obergrenze je Nachricht - aiohttps Vorgabe sind 4 MiB, und
|
||||
Base64 blaeht die Datei vorher um ein Drittel auf. Ueber die reale Instanz
|
||||
kommt noch Cloudflare samt Vorschaltserver dazu, die enger sein koennen.
|
||||
Genau das war der Befund vom 03.09.2026: dasselbe Cockpit-Foto ging in der
|
||||
Testinstanz (localhost) durch und scheiterte ueber die reale Instanz mit
|
||||
"connection lost" - eine abgerissene Verbindung, kein Fehler des Backends,
|
||||
deshalb auch keine brauchbare Meldung.
|
||||
|
||||
Verkleinern loest das an der Wurzel statt die Grenze zu verschieben: die
|
||||
groesste Flaeche, auf der ein Foto je erscheint, ist die Inhaltsspalte im
|
||||
Breitbildlayout mit 772px. 2000px lange Kante lassen dafuer reichlich Luft
|
||||
(auch fuer Bildschirme mit doppelter Punktdichte), und WebP bei 0,85 bringt
|
||||
ein solches Foto typischerweise auf einige hundert Kilobyte.
|
||||
|
||||
Faellt irgendein Schritt aus (Format nicht dekodierbar, kein WebP-Encoder,
|
||||
Ergebnis groesser als das Original), bleibt es beim Original - schlechter
|
||||
als vorher wird es dadurch nie. */
|
||||
const BILD_MAX_KANTE = 2000;
|
||||
const BILD_QUALITAET = 0.85;
|
||||
|
||||
function bildAlsBase64(blob) {
|
||||
return new Promise((fertig, fehler) => {
|
||||
const leser = new FileReader();
|
||||
leser.onload = () => fertig(String(leser.result).split(",")[1] || "");
|
||||
leser.onerror = () => fehler(leser.error);
|
||||
leser.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
/* Formate, die jeder Browser dieser App wirklich anzeigen kann.
|
||||
|
||||
Wichtig, weil das Backend nichts prueft: bilder.py schreibt die Bytes roh
|
||||
unter einen der elf festen .webp-Namen. Ein Foto, das der Browser nicht
|
||||
umwandeln konnte und das er auch nicht anzeigen kann, laege danach als
|
||||
Bilddatei auf der Platte, die niemand darstellt - ohne Meldung.
|
||||
|
||||
Der praktische Fall ist HEIC: die iPhone-Fotoauswahl liefert in der Regel
|
||||
JPEG, am Mac im Finder gewaehlt kommt aber eine .heic an, und die kann
|
||||
ausser Safari/WKWebView niemand dekodieren. */
|
||||
const BILD_SICHERE_TYPEN = /^image\/(jpeg|png|webp|gif|bmp|avif)$/i;
|
||||
const BILD_SICHERE_ENDUNGEN = /\.(jpe?g|png|webp|gif|bmp|avif)$/i;
|
||||
|
||||
function bildformatIstSicher(datei) {
|
||||
// Der MIME-Typ zaehlt, wenn er da ist. Aus dem Explorer gezogene Dateien
|
||||
// kommen je nach Quelle mit leerem type an - dann die Endung, dieselbe
|
||||
// Doppelpruefung wie bei istPdf().
|
||||
if (datei.type) return BILD_SICHERE_TYPEN.test(datei.type);
|
||||
return BILD_SICHERE_ENDUNGEN.test(datei.name);
|
||||
}
|
||||
|
||||
async function bildVerkleinern(datei) {
|
||||
let klein = null;
|
||||
const adresse = URL.createObjectURL(datei);
|
||||
try {
|
||||
const bild = await new Promise((fertig, fehler) => {
|
||||
const i = new Image();
|
||||
i.onload = () => fertig(i);
|
||||
i.onerror = () => fehler(new Error("Bild nicht lesbar"));
|
||||
i.src = adresse;
|
||||
});
|
||||
const kante = Math.max(bild.naturalWidth, bild.naturalHeight);
|
||||
if (kante) {
|
||||
const faktor = Math.min(1, BILD_MAX_KANTE / kante);
|
||||
const flaeche = document.createElement("canvas");
|
||||
flaeche.width = Math.round(bild.naturalWidth * faktor);
|
||||
flaeche.height = Math.round(bild.naturalHeight * faktor);
|
||||
flaeche.getContext("2d").drawImage(bild, 0, 0, flaeche.width, flaeche.height);
|
||||
klein = await new Promise((fertig) =>
|
||||
flaeche.toBlob(fertig, "image/webp", BILD_QUALITAET),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Bild konnte nicht umgewandelt werden", err);
|
||||
} finally {
|
||||
URL.revokeObjectURL(adresse);
|
||||
}
|
||||
|
||||
// Ein bereits sparsam gespeichertes Foto soll nicht ein zweites Mal durch
|
||||
// eine verlustbehaftete Kodierung - das kostet Qualitaet ohne Gewinn.
|
||||
if (klein && klein.size < datei.size) return klein;
|
||||
|
||||
/* Umwandeln ging nicht (oder brachte nichts) - dann muss wenigstens das
|
||||
Original anzeigbar sein. Sonst lieber jetzt eine klare Meldung als
|
||||
hinterher ein leeres Bild, dessen Ursache niemand mehr sieht. */
|
||||
if (!bildformatIstSicher(datei)) {
|
||||
const art = datei.type || datei.name.split(".").pop() || "unbekannt";
|
||||
throw new Error(
|
||||
`Dieses Bildformat kann der Browser nicht umwandeln (${art}). ` +
|
||||
"Bitte das Foto als JPEG, PNG oder WebP sichern und noch einmal versuchen.",
|
||||
);
|
||||
}
|
||||
return datei;
|
||||
}
|
||||
function bildInfo(i) {
|
||||
const b = BILDER[i];
|
||||
if (b.name === "Seitenansicht" && CAR.reifen.aktiv === "Winter") {
|
||||
@@ -5937,16 +6034,15 @@ function ereignisseVerdrahten() {
|
||||
if (e.target.dataset.bildupload !== undefined) {
|
||||
const datei = e.target.files[0]; if (!datei) return;
|
||||
const dateiname = e.target.dataset.bildupload;
|
||||
const leser = new FileReader();
|
||||
leser.onload = async () => {
|
||||
const daten_base64 = String(leser.result).split(",")[1] || "";
|
||||
e.target.value = "";
|
||||
(async () => {
|
||||
try {
|
||||
const klein = await bildVerkleinern(datei);
|
||||
const daten_base64 = await bildAlsBase64(klein);
|
||||
await HASS.callService(DOMAIN, "bild_hochladen", { dateiname, daten_base64 });
|
||||
} catch (err) { hinweis("Hochladen fehlgeschlagen", err.message); }
|
||||
bildMenuOffen = null; bildVersion = Date.now(); render();
|
||||
};
|
||||
leser.readAsDataURL(datei);
|
||||
e.target.value = "";
|
||||
})();
|
||||
return;
|
||||
}
|
||||
if (e.target.dataset.backupintervall !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user