Zugaenge: die Dienste-Token aus App und Panel bedienbar (2026.9.3.8)
Die Token liegen seit jeher in entry.options, geschrieben vom Options-Flow unter Einstellungen -> Geraete & Dienste -> Konfigurieren. Der Weg bleibt und ist der Rueckfall, wenn die App den Server nicht erreicht - genau der Fall, in dem ein abgelaufener Token auffaellt. Erreichbar ist er aber nur ueber HAs eigene Weboberflaeche; die iOS-App zeigt keine Konfigurationsdialoge. Die neue Kachel ist die Bedienflaeche dafuer, kein zweiter Speicherort. Der Wert kommt nie zurueck: veroeffentlicht werden nur "gesetzt/nicht gesetzt" und die letzten vier Zeichen (unter zwoelf Zeichen Laenge gar nichts). Auch das Protokoll bekommt ihn nicht, und das Panel leert das Eingabefeld sofort nach dem Absenden. Bewusst NICHT im Fahrzeugprofil: sicherung.py sichert fahrzeugprofil.json, und "Backup exportieren" laedt es als Datei auf das Geraet - ein Token dort wanderte in jede Sicherung und jede weitergegebene Datei. Ein Dienst statt zwei: zugang_setzen(dienst, token), leerer Text loescht. In der App nicht ueber die Warteschlange - ein Stunden spaeter nachgereichter Token ueberschriebe womoeglich einen inzwischen eingetragenen neuen. CONF_FLESPI_TOKEN ist neu und noch ohne Leser; der Flespi-Weg zum Auslesen der Geraetekonfiguration ist damit vorbereitet, aber nicht gebaut. Verifiziert: py_compile auf sechs Backend-Dateien, Panel als Modul geparst, companion-app tsc sauber und 179/179, audi_ha_test auf 2026.9.3.8 sauber gestartet. Live am laufenden Panel der ganze Kreis, Container hinterher wie vorgefunden: Wegwerf-Token gesetzt -> "gesetzt . ...0000", der Wert selbst taucht in den veroeffentlichten Attributen nirgends auf (darauf geprueft), ueber die Sicherheitsabfrage geloescht -> wieder "nicht gesetzt", der Gitea-Token unberuehrt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -341,6 +341,16 @@ export class DataMetricApi {
|
||||
return this.rest.dienstAufrufen(DIENST_DOMAIN, "fahrt_jetzt_beenden", {});
|
||||
}
|
||||
|
||||
/** Traegt einen Dienste-Token ein oder loescht ihn (leerer Text = loeschen).
|
||||
|
||||
Nicht ueber die Warteschlange: ein Token, der Stunden spaeter aus dem
|
||||
Funkloch nachgereicht wird, ueberschriebe womoeglich einen inzwischen
|
||||
eingetragenen neuen. Und der Wert kommt nie zurueck - was gesetzt ist,
|
||||
steht in daten.zugaenge, der Token selbst nirgends. */
|
||||
zugangSetzen(dienst: string, token: string): Promise<unknown> {
|
||||
return this.rest.dienstAufrufen(DIENST_DOMAIN, "zugang_setzen", { dienst, token });
|
||||
}
|
||||
|
||||
historieImportieren(start: string, ende: string): Promise<unknown> {
|
||||
return this.rest.dienstAufrufen(DIENST_DOMAIN, "historie_importieren", {
|
||||
start,
|
||||
|
||||
@@ -219,11 +219,27 @@ export interface IntegrationUpdateAngabe {
|
||||
fehler: string | null;
|
||||
}
|
||||
|
||||
/** Ein Dienste-Token dieser Installation (zugaenge.py).
|
||||
|
||||
Der Token selbst ist hier BEWUSST nicht enthalten und kommt auch nie ueber
|
||||
die Leitung: gezeigt wird nur, ob einer gesetzt ist, und seine letzten vier
|
||||
Zeichen zum Wiedererkennen. */
|
||||
export interface ZugangAngabe {
|
||||
dienst: string;
|
||||
titel: string;
|
||||
zweck: string;
|
||||
/** Laeuft die App ohne diesen Token gar nicht? Beide heutigen tun es. */
|
||||
erforderlich: boolean;
|
||||
gesetzt: boolean;
|
||||
rest: string | null;
|
||||
}
|
||||
|
||||
/** Nutzlast von sensor.audi_dashboard_app_version. */
|
||||
export interface AppVersionAngabe {
|
||||
app: string | null;
|
||||
buendel: Buendelangabe | null;
|
||||
integration_update: IntegrationUpdateAngabe | null;
|
||||
zugaenge?: ZugangAngabe[];
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- Entitäts-Verzeichnis */
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
type Fahrt,
|
||||
type Fahrzeugstatus,
|
||||
type IntegrationUpdateAngabe,
|
||||
type ZugangAngabe,
|
||||
type Profil,
|
||||
type Tankvorgang,
|
||||
type Verbindungszustand,
|
||||
@@ -69,6 +70,7 @@ export interface DatenWert {
|
||||
/** Ergebnis der letzten Selbst-Update-Prüfung/-Installation der
|
||||
Integration (aktualisierung.py), oder null vor der ersten Prüfung. */
|
||||
integrationUpdate: IntegrationUpdateAngabe | null
|
||||
zugaenge: ZugangAngabe[]
|
||||
|
||||
einstellungen: Einstellungen | null
|
||||
fahrzeug: Fahrzeug | null
|
||||
@@ -115,6 +117,7 @@ export function DatenAnbieter({
|
||||
const [warteschlange, setzeWarteschlange] = useState<readonly WartenderAuftrag[]>([])
|
||||
const [serverVersion, setzeServerVersion] = useState<string | null>(null)
|
||||
const [otaBuendel, setzeOtaBuendel] = useState<Buendelangabe | null>(null)
|
||||
const [zugaenge, setzeZugaenge] = useState<ZugangAngabe[]>([])
|
||||
const [integrationUpdate, setzeIntegrationUpdate] = useState<IntegrationUpdateAngabe | null>(
|
||||
null,
|
||||
)
|
||||
@@ -158,6 +161,7 @@ export function DatenAnbieter({
|
||||
setzeServerVersion(versionsangabe?.app ?? null)
|
||||
setzeOtaBuendel(versionsangabe?.buendel ?? null)
|
||||
setzeIntegrationUpdate(versionsangabe?.integration_update ?? null)
|
||||
setzeZugaenge(versionsangabe?.zugaenge ?? [])
|
||||
return true
|
||||
} catch (fehler) {
|
||||
// Ein voruebergehender Ausfall ist kein Ladefehler, solange schon
|
||||
@@ -289,6 +293,7 @@ export function DatenAnbieter({
|
||||
serverVersion,
|
||||
otaBuendel,
|
||||
integrationUpdate,
|
||||
zugaenge,
|
||||
einstellungen,
|
||||
fahrzeug,
|
||||
fahrten,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
import { ActionButton, Feld, Seg, Sheet, Slider, Switch, Tile } from "@audi-dash/ui"
|
||||
import { Accordion, ActionButton, Feld, Seg, Sheet, Slider, Switch, Tile } from "@audi-dash/ui"
|
||||
|
||||
import { ApiFehler, DIENST_DOMAIN, zugangLesen, zugangVerwerfen } from "../api"
|
||||
import type { Profil } from "../api"
|
||||
@@ -42,6 +42,7 @@ export function Einstellungen({
|
||||
otaBuendel,
|
||||
integrationUpdate,
|
||||
serverVersion,
|
||||
zugaenge,
|
||||
verbindung,
|
||||
api,
|
||||
profilSpeichern,
|
||||
@@ -223,6 +224,29 @@ export function Einstellungen({
|
||||
}
|
||||
}
|
||||
|
||||
/* Zugaenge: je Dienst ein Entwurf im Eingabefeld. Der gespeicherte Token
|
||||
kommt NIE zurueck (siehe zugaenge.py), das Feld startet deshalb immer leer
|
||||
und zeigt als Platzhalter, ob schon einer hinterlegt ist. */
|
||||
const [tokenEntwurf, setzeTokenEntwurf] = useState<Record<string, string>>({})
|
||||
const [tokenLaeuft, setzeTokenLaeuft] = useState<string | null>(null)
|
||||
const [tokenFehler, setzeTokenFehler] = useState<string | null>(null)
|
||||
|
||||
const tokenSchreiben = async (dienst: string, wert: string) => {
|
||||
setzeTokenLaeuft(dienst)
|
||||
setzeTokenFehler(null)
|
||||
try {
|
||||
await api.zugangSetzen(dienst, wert)
|
||||
setzeTokenEntwurf((alt) => ({ ...alt, [dienst]: "" }))
|
||||
await neuLaden()
|
||||
} catch (fehler) {
|
||||
setzeTokenFehler(
|
||||
fehler instanceof Error ? fehler.message : "Der Token konnte nicht gespeichert werden.",
|
||||
)
|
||||
} finally {
|
||||
setzeTokenLaeuft(null)
|
||||
}
|
||||
}
|
||||
|
||||
const abmelden = async () => {
|
||||
if (!bestaetigen("Zugang von diesem Gerät entfernen? Die Daten auf dem Server bleiben.")) return
|
||||
await zugangVerwerfen()
|
||||
@@ -655,6 +679,66 @@ export function Einstellungen({
|
||||
</Tile>
|
||||
)}
|
||||
|
||||
{zugaenge.length > 0 && (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Zugänge</span>
|
||||
{/* Aufklappbar, weil die Token im Alltag nichts zu suchen haben —
|
||||
man fasst sie an, wenn einer abgelaufen ist, und sonst nie. */}
|
||||
{zugaenge.map((z) => (
|
||||
<Accordion
|
||||
key={z.dienst}
|
||||
title={z.titel}
|
||||
summary={
|
||||
<span className={z.gesetzt ? "dm-token-gesetzt" : "dm-token-offen"}>
|
||||
{z.gesetzt ? (z.rest ? `gesetzt · …${z.rest}` : "gesetzt") : "nicht gesetzt"}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<p className="dm-erklaerung">{z.zweck}</p>
|
||||
<Feld label="Neuer Token" last>
|
||||
<input
|
||||
className="dm-eingabe"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
placeholder={z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}
|
||||
value={tokenEntwurf[z.dienst] ?? ""}
|
||||
onChange={(e) =>
|
||||
setzeTokenEntwurf((alt) => ({ ...alt, [z.dienst]: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</Feld>
|
||||
<div className="dm-knopfreihe">
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
disabled={tokenLaeuft !== null || !(tokenEntwurf[z.dienst] ?? "").trim()}
|
||||
onClick={() => void tokenSchreiben(z.dienst, tokenEntwurf[z.dienst] ?? "")}
|
||||
>
|
||||
{tokenLaeuft === z.dienst ? "Speichert …" : "Speichern"}
|
||||
</ActionButton>
|
||||
{z.gesetzt && (
|
||||
<ActionButton
|
||||
disabled={tokenLaeuft !== null}
|
||||
onClick={() => {
|
||||
if (bestaetigen(`Token für ${z.titel} löschen?`)) {
|
||||
void tokenSchreiben(z.dienst, "")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</Accordion>
|
||||
))}
|
||||
{tokenFehler && <p className="dm-fehler">{tokenFehler}</p>}
|
||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||
Ein gespeicherter Token wird nie wieder angezeigt — nur ob einer hinterlegt ist.
|
||||
Dieselben Felder gibt es in Home Assistant unter Einstellungen → Geräte &
|
||||
Dienste → Audi Dashboard → Konfigurieren.
|
||||
</span>
|
||||
</Tile>
|
||||
)}
|
||||
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Version</span>
|
||||
<Werteliste
|
||||
|
||||
@@ -1992,3 +1992,6 @@ button.dm-listenzeile {
|
||||
/* Gruen wie der Sicherheits-Haken: es laeuft gerade etwas. */
|
||||
.dm-blatt__k small .dm-laeuft, .dm-laeuft { color: var(--ok); }
|
||||
.dm-vorlaeufig { color: var(--fg3); }
|
||||
|
||||
.dm-token-gesetzt { color: var(--ok); }
|
||||
.dm-token-offen { color: var(--fg3); }
|
||||
|
||||
Reference in New Issue
Block a user