vCard-Import fürs Autohaus, Selbst-Update-Logging und UI-Feinschliff

- Service -> Autohaus -> Zahnrad hat jetzt einen "vCard importieren"-Knopf
  (öffnet den Dateiauswahldialog für .vcf). Liest FN/ORG, TEL, EMAIL, ADR
  und befüllt das Formular - in beiden Frontends, jeweils passend zum
  bestehenden Speicherverhalten (Panel speichert sofort, companion-app
  erst über den vorhandenen "Speichern"-Knopf).
- update_pruefen/update_installieren loggten bisher nur bei einem Fehler -
  ein erfolgreicher Aufruf war im Log nicht von "noch nicht ausgeführt" zu
  unterscheiden. Jetzt auch eine Erfolgsmeldung.
- Panel: der feste Hinweis auf den fehlenden Gitea-Token stand dauerhaft in
  der Integration-Update-Kachel, auch wenn ein Token längst hinterlegt war.
  Erscheint jetzt nur noch als Teil einer echten Fehlermeldung.

Details in AGENTS.md, Abschnitte J (Nachtrag) und K.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:30:24 +02:00
parent fb251154c7
commit 7365180550
9 changed files with 289 additions and 7 deletions
@@ -228,6 +228,10 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
jetzt = datetime.datetime.now(datetime.timezone.utc).isoformat()
try:
ergebnis = await aktualisierung.version_pruefen(hass, token, k.version)
_LOGGER.info(
"Update-Prüfung: Gitea hat Version %s (installiert: %s, verfügbar: %s)",
ergebnis["version"], k.version, ergebnis["verfuegbar"],
)
k.letzte_update_pruefung = {
"verfuegbar": ergebnis["verfuegbar"],
"version": ergebnis["version"],
@@ -255,6 +259,10 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
jetzt = datetime.datetime.now(datetime.timezone.utc).isoformat()
try:
ergebnis = await aktualisierung.update_installieren(hass, token)
_LOGGER.info(
"Update installiert: Version %s - Home Assistant muss neu gestartet werden.",
ergebnis["version"],
)
k.letzte_update_pruefung = {
"verfuegbar": False,
"version": ergebnis["version"],
@@ -1 +1 @@
{"version":"2026.8.24.4","sha256":"cbce1baa785a3577fc65168b9f8b2094ecd844aadc2ac36f99d6be8ccb4cb8c1","bytes":231567,"gebaut":"2026-08-24T09:39:59Z"}
{"version":"2026.8.24.5","sha256":"378a1cbc7f5daa3d8c5d74734a4c09f30557b243a8ce4d720240bf911fbeff76","bytes":232096,"gebaut":"2026-08-24T10:29:33Z"}
@@ -1969,6 +1969,44 @@ function termine() {
];
}
/* Liest Name, Adresse, Telefon und E-Mail aus einer vCard (.vcf) - deckt
vCard 3.0/4.0 so weit ab, wie ein typischer Werkstatt-Kontakt aus dem
Adressbuch (iOS/Android/Outlook) sie befüllt. Kein vollständiger Parser,
nur die vier Felder, die das Autohaus-Formular kennt.
Gefaltete Zeilen (Fortsetzung mit führendem Leerzeichen/Tab, RFC 6350
§3.2) werden vor dem zeilenweisen Parsen entfaltet; ADR setzt sich aus
PO-Box, Zusatz, Straße, Ort, Region, PLZ, Land zusammen (in dieser
Reihenfolge, mit Semikolon getrennt) - hier zu einer lesbaren
Ein-Zeilen-Adresse zusammengefasst, weil das Formularfeld nur eine
Zeile kennt. Mehrfache TEL/EMAIL/ADR-Zeilen: die erste gewinnt. */
function vcardParsen(text) {
const entfaltet = String(text).replace(/\r\n/g, "\n").replace(/\n[ \t]/g, "");
const zeilen = entfaltet.split("\n").map((z) => z.trim()).filter(Boolean);
const entwerten = (s) => s.replace(/\\n/gi, "\n").replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
let name = null, org = null, tel = null, mail = null, adresse = null;
for (const zeile of zeilen) {
const doppelpunkt = zeile.indexOf(":");
if (doppelpunkt < 0) continue;
const eigenschaft = zeile.slice(0, doppelpunkt).split(";")[0].toUpperCase();
const wert = zeile.slice(doppelpunkt + 1);
if (eigenschaft === "FN" && !name) name = entwerten(wert);
else if (eigenschaft === "ORG" && !org) org = entwerten(wert.split(";")[0]);
else if (eigenschaft === "TEL" && !tel) tel = entwerten(wert);
else if (eigenschaft === "EMAIL" && !mail) mail = entwerten(wert);
else if (eigenschaft === "ADR" && !adresse) {
const teile = wert.split(";").map(entwerten);
const strasse = [teile[1], teile[2]].filter(Boolean).join(" ");
const ort = [teile[5], teile[3]].filter(Boolean).join(" ");
adresse = [strasse, ort, teile[6]].filter(Boolean).join(", ") || null;
}
}
if (!name && !org && !tel && !mail && !adresse) {
throw new Error("Keine lesbaren Kontaktfelder gefunden - ist das eine .vcf-Datei?");
}
return { name: name || org || null, adresse, tel, mail };
}
function vWerkstatt() {
const a = CAR.service.autohaus;
return `
@@ -1978,6 +2016,8 @@ function vWerkstatt() {
<div class="feld"><label>Telefon</label><input type="tel" value="${esc(a.tel)}" data-ah="tel"></div>
<div class="feld" style="border-bottom:0"><label>E-Mail</label>
<input type="email" value="${esc(a.mail || "")}" data-ah="mail" placeholder="noch offen"></div>
<label class="aktion" style="display:block;text-align:center;margin-top:16px;cursor:pointer">
vCard importieren<input type="file" accept=".vcf,text/vcard" data-vcard-import hidden></label>
</div>`;
}
@@ -2405,9 +2445,7 @@ function vEinst() {
<button class="aktion" style="margin-top:14px" data-update="installieren">Update installieren</button>`;
}
return `<span style="display:block;margin-top:8px;color:var(--fg2);font-size:13.5px;line-height:1.6">
${u ? `Integration ist aktuell${u.geprueft_am ? ` (geprüft ${dedat(new Date(u.geprueft_am))})` : ""}.` : "Noch nicht geprüft."}
Ein Gitea-Zugriffstoken muss zuvor unter Einstellungen -> Geräte & Dienste -> Audi
Dashboard -> Konfigurieren eingetragen sein.</span>
${u ? `Integration ist aktuell${u.geprueft_am ? ` (geprüft ${dedat(new Date(u.geprueft_am))})` : ""}.` : "Noch nicht geprüft."}</span>
<button class="aktion" style="margin-top:14px" data-update="pruefen">Auf Update prüfen</button>`;
})()}
${INTEGRATION_UPDATE && INTEGRATION_UPDATE.fehler
@@ -4612,6 +4650,23 @@ function ereignisseVerdrahten() {
leser.readAsText(datei);
return;
}
if (e.target.dataset.vcardImport !== undefined) {
const datei = e.target.files[0]; if (!datei) return;
const leser = new FileReader();
leser.onload = () => {
try {
const felder = vcardParsen(leser.result);
if (felder.name) CAR.service.autohaus.name = felder.name;
if (felder.adresse) CAR.service.autohaus.adresse = felder.adresse;
if (felder.tel) CAR.service.autohaus.tel = felder.tel;
if (felder.mail) CAR.service.autohaus.mail = felder.mail;
profilSpeichern(); render();
} catch (err) { hinweis("vCard konnte nicht gelesen werden", err.message); }
};
leser.readAsText(datei);
e.target.value = "";
return;
}
if (e.target.dataset.bildupload !== undefined) {
const datei = e.target.files[0]; if (!datei) return;
const dateiname = e.target.dataset.bildupload;
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.8.24.4",
"version": "2026.8.24.5",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"],