flespi: echtes Lesen statt Zwischenspeicher, und kein Gleichstand ohne Grundlage
Der Waechter meldete am 04.09.2026 "stimmt ueberein" fuer
trip_scenario.ign_off_timeout: 900 gegen unsere Konstante NACHLAUF_S = 900.
Das Geraet hat dort 0 - das Trip-Szenario ist abgeschaltet. flespis
Zwischenspeicher trug den Stand vom 01.09. 13:15, dreieinhalb Tage alt.
Der Eigentuemer hat es aufgedeckt: "die gelesenen werte sind veraltet".
Zwei Aenderungen:
1. "Jetzt lesen" leert erst den Zwischenspeicher der ueberwachten
Einstellungen (DELETE /gw/devices/{id}/settings/{name} - die
API-Entsprechung des Panel-Knopfes "clear cache and synchronize"; loescht
den gespeicherten Wert, nicht den im Geraet) und holt dann neu.
Einstellungen mit einem AUSSTEHENDEN Wert werden uebersprungen: dasselbe
DELETE wuerde ihn verwerfen, und die Aenderung kaeme nie an - wie
1003/1004, die am 04.09. acht Stunden in der Warteschlange standen.
2. `abweichung` ist dreiwertig: true, false und **null (kein Urteil)**. Null
steht dort, wo ein Vergleich nichts aussagt - der Wert wurde nie vom
Geraet bestaetigt, oder es ist gerade eine Aenderung unterwegs. Dazu
traegt jede Zeile `gemeldet_am` (flespis `updated`), und beide
Oberflaechen zeigen es an: "bestaetigt 01.09.2026 (4 T. alt)" bzw.
"nie bestaetigt".
Ein stiller Gleichstand mit einem veralteten Wert ist schlimmer als gar kein
Vergleich: er behauptet Sicherheit, wo keine ist.
tsc sauber, 271 Tests, vite build sauber, Panel als Modul geparst,
py_compile sauber, audi_ha_test auf 2026.9.4.26 ohne Traceback.
This commit is contained in:
@@ -255,7 +255,15 @@ export interface DongleWert {
|
|||||||
offen: unknown;
|
offen: unknown;
|
||||||
/** Unser eigener Wert, sofern es einen zu vergleichen gibt. */
|
/** Unser eigener Wert, sofern es einen zu vergleichen gibt. */
|
||||||
unser: number | null;
|
unser: number | null;
|
||||||
abweichung: boolean;
|
/** Dreiwertig: true weicht ab, false stimmt überein, **null kein
|
||||||
|
Urteil** — der Wert wurde nie vom Gerät bestätigt, oder es ist
|
||||||
|
gerade eine Änderung zu ihm unterwegs. Bis zum 04.09.2026 war das
|
||||||
|
ein Bool, und `false` behauptete Übereinstimmung auch gegen einen
|
||||||
|
tagealten Wert. */
|
||||||
|
abweichung: boolean | null;
|
||||||
|
/** Wann das Gerät diesen Wert zuletzt bestätigt hat (flespis
|
||||||
|
`updated`) — null heißt: nie. */
|
||||||
|
gemeldet_am?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DongleStand {
|
export interface DongleStand {
|
||||||
@@ -264,6 +272,10 @@ export interface DongleStand {
|
|||||||
anzahl?: number;
|
anzahl?: number;
|
||||||
werte?: DongleWert[];
|
werte?: DongleWert[];
|
||||||
abweichungen?: number;
|
abweichungen?: number;
|
||||||
|
/** Zeilen ohne Urteil: nie bestätigt oder gerade unterwegs. */
|
||||||
|
unbekannt?: number;
|
||||||
|
/** Welche Zwischenspeicher dieser Abruf geleert hat. */
|
||||||
|
geleert?: string[];
|
||||||
/** Wie viele der gezeigten Werte noch zum Gerät unterwegs sind. */
|
/** Wie viele der gezeigten Werte noch zum Gerät unterwegs sind. */
|
||||||
offen?: number;
|
offen?: number;
|
||||||
fehler: string | null;
|
fehler: string | null;
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ import { HistorienImport } from "./HistorienImport"
|
|||||||
import type { ServicebuchEintrag } from "./Service"
|
import type { ServicebuchEintrag } from "./Service"
|
||||||
import { zugangLesenSynchron, zugangVergessen } from "./zugang"
|
import { zugangLesenSynchron, zugangVergessen } from "./zugang"
|
||||||
|
|
||||||
|
/* Wann das Gerät einen Wert zuletzt bestätigt hat — oder dass es das nie
|
||||||
|
getan hat. Steht dort, wo vorher nichts stand, wenn kein Urteil möglich
|
||||||
|
ist: ein stiller Gleichstand mit einem veralteten Wert ist schlimmer als
|
||||||
|
gar kein Vergleich (04.09.2026). Wortgleich mit dongleStand() im Panel. */
|
||||||
|
function dongleStand(w: { gemeldet_am?: string | null }): string {
|
||||||
|
if (!w.gemeldet_am) return "nie bestätigt"
|
||||||
|
const d = new Date(w.gemeldet_am)
|
||||||
|
if (Number.isNaN(d.getTime())) return "nie bestätigt"
|
||||||
|
const tage = Math.floor((Date.now() - d.getTime()) / 86400000)
|
||||||
|
const wann = datum(d)
|
||||||
|
return tage >= 1 ? `bestätigt ${wann} (${tage} T. alt)` : `bestätigt ${wann}`
|
||||||
|
}
|
||||||
|
|
||||||
export function Einstellungen({
|
export function Einstellungen({
|
||||||
tabBeschriftung,
|
tabBeschriftung,
|
||||||
setzeTabBeschriftung,
|
setzeTabBeschriftung,
|
||||||
@@ -964,13 +977,13 @@ export function Einstellungen({
|
|||||||
key={w.schluessel}
|
key={w.schluessel}
|
||||||
label={`${w.titel} ${w.nummer}`}
|
label={`${w.titel} ${w.nummer}`}
|
||||||
wert={`${w.geraet ?? "–"}${w.einheit ? " " + w.einheit : ""}`}
|
wert={`${w.geraet ?? "–"}${w.einheit ? " " + w.einheit : ""}`}
|
||||||
warnung={Boolean(w.abweichung)}
|
warnung={w.abweichung === true}
|
||||||
zusatz={
|
zusatz={
|
||||||
w.offen != null
|
w.offen != null
|
||||||
? `→ ${w.offen}, unterwegs`
|
? `→ ${w.offen}, unterwegs`
|
||||||
: w.abweichung
|
: w.abweichung === true
|
||||||
? `(wir: ${w.unser})`
|
? `(wir: ${w.unser})`
|
||||||
: undefined
|
: dongleStand(w)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -269,6 +269,23 @@ def _sicht(eintraege: list[dict], feld: str) -> dict[str, object]:
|
|||||||
return flach
|
return flach
|
||||||
|
|
||||||
|
|
||||||
|
def _stempel(eintraege: list[dict]) -> dict[str, float]:
|
||||||
|
"""Wann das GERAET jede Einstellung zuletzt bestaetigt hat.
|
||||||
|
|
||||||
|
flespi legt zu jedem Eintrag ein `updated` (Unix-Sekunde). Das ist der
|
||||||
|
Zeitpunkt, zu dem der Wert vom Geraet kam - nicht der unseres Abrufs.
|
||||||
|
Genau daran fehlte es am 04.09.2026: `trip_scenario` stand auf dem Stand
|
||||||
|
vom 01.09. 13:15, waehrend die aufgespielte Konfiguration das Szenario
|
||||||
|
laengst abgeschaltet hatte. Der Waechter verglich 900 gegen 900 und meldete
|
||||||
|
Uebereinstimmung - gegen einen dreieinhalb Tage alten Wert.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
e["name"]: e["updated"]
|
||||||
|
for e in eintraege
|
||||||
|
if e.get("name") and isinstance(e.get("updated"), (int, float))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _roh(eintraege: list[dict], name: str) -> dict | None:
|
def _roh(eintraege: list[dict], name: str) -> dict | None:
|
||||||
for eintrag in eintraege:
|
for eintrag in eintraege:
|
||||||
if eintrag.get("name") == name:
|
if eintrag.get("name") == name:
|
||||||
@@ -281,8 +298,20 @@ def vergleichen(
|
|||||||
aktuell: dict[str, object],
|
aktuell: dict[str, object],
|
||||||
offen: dict[str, object],
|
offen: dict[str, object],
|
||||||
wunsch: int | None,
|
wunsch: int | None,
|
||||||
|
stempel: dict[str, float] | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Die interessanten Einstellungen mit unserem eigenen Wert daneben."""
|
"""Die interessanten Einstellungen mit unserem eigenen Wert daneben.
|
||||||
|
|
||||||
|
`abweichung` ist DREIWERTIG: True (weicht ab), False (stimmt ueberein) und
|
||||||
|
**None (kein Urteil)**. None steht dort, wo ein Vergleich nichts aussagen
|
||||||
|
wuerde - weil das Geraet den Wert nie bestaetigt hat, oder weil gerade eine
|
||||||
|
Aenderung zu ihm unterwegs ist und der Wert sich ohnehin gleich aendert.
|
||||||
|
|
||||||
|
Warum das noetig wurde: bis zum 04.09.2026 war `abweichung` ein blosses
|
||||||
|
Bool, und `False` hiess "stimmt ueberein" - auch dann, wenn der verglichene
|
||||||
|
Wert Tage alt war. Ein stiller Gleichstand mit einem veralteten Wert ist
|
||||||
|
schlimmer als gar kein Vergleich: er behauptet Sicherheit, wo keine ist.
|
||||||
|
"""
|
||||||
from . import fahrterkennung
|
from . import fahrterkennung
|
||||||
|
|
||||||
zeilen = []
|
zeilen = []
|
||||||
@@ -298,11 +327,17 @@ def vergleichen(
|
|||||||
unser = None
|
unser = None
|
||||||
# Nur vergleichen, wenn beide Zahlen sind - ein Modus wie "Deep Sleep"
|
# Nur vergleichen, wenn beide Zahlen sind - ein Modus wie "Deep Sleep"
|
||||||
# hat kein Gegenstück bei uns.
|
# hat kein Gegenstück bei uns.
|
||||||
abweichung = (
|
# Der Name der Einstellung ist der Teil vor dem ersten Punkt -
|
||||||
isinstance(geraet, (int, float))
|
# "sleep_mode.mode.timeout" gehoert zu "sleep_mode".
|
||||||
and isinstance(unser, (int, float))
|
name = schluessel.split(".")[0]
|
||||||
and int(geraet) != int(unser)
|
gemeldet = (stempel or {}).get(name)
|
||||||
)
|
wartet = schluessel in offen and offen.get(schluessel) != geraet
|
||||||
|
vergleichbar = isinstance(geraet, (int, float)) and isinstance(unser, (int, float))
|
||||||
|
if not vergleichbar or gemeldet is None or wartet:
|
||||||
|
# Kein Gegenstueck bei uns, nie bestaetigt, oder gerade unterwegs.
|
||||||
|
abweichung: bool | None = None
|
||||||
|
else:
|
||||||
|
abweichung = int(geraet) != int(unser)
|
||||||
zeilen.append(
|
zeilen.append(
|
||||||
{
|
{
|
||||||
"schluessel": schluessel,
|
"schluessel": schluessel,
|
||||||
@@ -322,6 +357,13 @@ def vergleichen(
|
|||||||
),
|
),
|
||||||
"unser": unser,
|
"unser": unser,
|
||||||
"abweichung": abweichung,
|
"abweichung": abweichung,
|
||||||
|
# Wann das Geraet diesen Wert zuletzt bestaetigt hat. None
|
||||||
|
# heisst: nie - dann steht daneben kein Urteil, sondern nichts.
|
||||||
|
"gemeldet_am": (
|
||||||
|
datetime.datetime.fromtimestamp(gemeldet, datetime.UTC).isoformat()
|
||||||
|
if gemeldet is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return zeilen
|
return zeilen
|
||||||
@@ -357,26 +399,97 @@ async def _alle_einstellungen(
|
|||||||
return eintraege
|
return eintraege
|
||||||
|
|
||||||
|
|
||||||
def _stand(geraet: int, eintraege: list[dict], wunsch: int | None) -> dict:
|
def _stand(
|
||||||
|
geraet: int,
|
||||||
|
eintraege: list[dict],
|
||||||
|
wunsch: int | None,
|
||||||
|
geleert: list[str] | None = None,
|
||||||
|
) -> dict:
|
||||||
aktuell = _sicht(eintraege, "current")
|
aktuell = _sicht(eintraege, "current")
|
||||||
offen = _sicht(eintraege, "pending")
|
offen = _sicht(eintraege, "pending")
|
||||||
werte = vergleichen(aktuell, offen, wunsch)
|
werte = vergleichen(aktuell, offen, wunsch, _stempel(eintraege))
|
||||||
return {
|
return {
|
||||||
"geraet": geraet,
|
"geraet": geraet,
|
||||||
"gelesen_am": jetzt_iso(),
|
"gelesen_am": jetzt_iso(),
|
||||||
"anzahl": len(eintraege),
|
"anzahl": len(eintraege),
|
||||||
"werte": werte,
|
"werte": werte,
|
||||||
"abweichungen": sum(1 for z in werte if z["abweichung"]),
|
# Welche Zwischenspeicher dieser Abruf geleert hat - leer, wenn nur
|
||||||
|
# gelesen wurde.
|
||||||
|
"geleert": geleert or [],
|
||||||
|
"abweichungen": sum(1 for z in werte if z["abweichung"] is True),
|
||||||
|
# Zeilen ohne Urteil: nie bestaetigt oder gerade unterwegs.
|
||||||
|
"unbekannt": sum(1 for z in werte if z["abweichung"] is None),
|
||||||
"offen": sum(1 for z in werte if z["offen"] is not None),
|
"offen": sum(1 for z in werte if z["offen"] is not None),
|
||||||
"fehler": None,
|
"fehler": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def lesen(k: Koordinator) -> dict:
|
# Die Namen der Einstellungen, die wir ueberwachen - der Teil vor dem ersten
|
||||||
"""Holt die Konfiguration und gibt den fertigen Anzeigestand zurück."""
|
# Punkt in INTERESSANT ("sleep_mode.mode.timeout" -> "sleep_mode").
|
||||||
|
UEBERWACHT = sorted({schluessel.split(".")[0] for schluessel in INTERESSANT})
|
||||||
|
|
||||||
|
|
||||||
|
async def _cache_leeren(
|
||||||
|
hass: HomeAssistant, token: str, geraet: int, eintraege: list[dict]
|
||||||
|
) -> list[str]:
|
||||||
|
"""Loescht flespis Zwischenspeicher fuer die ueberwachten Einstellungen.
|
||||||
|
|
||||||
|
WAS DAS TUT UND WAS NICHT
|
||||||
|
-------------------------
|
||||||
|
`DELETE /gw/devices/{id}/settings/{name}` wirft den GESPEICHERTEN Wert weg,
|
||||||
|
nicht den im Geraet. flespi fragt ihn beim naechsten Verbinden neu ab. Es
|
||||||
|
ist die API-Entsprechung des Knopfes "clear cache and synchronize" im
|
||||||
|
flespi-Panel.
|
||||||
|
|
||||||
|
Ohne das liest jeder Abruf nur den Zwischenspeicher zurueck. Am 04.09.2026
|
||||||
|
stand dort fuer `trip_scenario` der Stand vom 01.09. 13:15 - dreieinhalb
|
||||||
|
Tage alt, und das Szenario war laengst abgeschaltet.
|
||||||
|
|
||||||
|
AUSSTEHENDE WERTE BLEIBEN UNANGETASTET
|
||||||
|
--------------------------------------
|
||||||
|
Dasselbe DELETE verwirft auch ein `pending`. Stuende gerade eine Aenderung
|
||||||
|
in der Warteschlange zum Geraet - wie `1003`/`1004` am 04.09. acht Stunden
|
||||||
|
lang -, waere sie damit weg, ohne dass es jemand merkt. Solche
|
||||||
|
Einstellungen werden uebersprungen; ihr Wert aendert sich ohnehin gleich.
|
||||||
|
"""
|
||||||
|
geleert: list[str] = []
|
||||||
|
for eintrag in eintraege:
|
||||||
|
name = eintrag.get("name")
|
||||||
|
if name not in UEBERWACHT:
|
||||||
|
continue
|
||||||
|
if eintrag.get("pending") is not None:
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s hat einen ausstehenden Wert - Zwischenspeicher nicht "
|
||||||
|
"geleert, sonst waere die Aenderung verworfen", name,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
await _anfrage(hass, token, "DELETE", f"/gw/devices/{geraet}/settings/{name}")
|
||||||
|
except FlespiFehler as fehler:
|
||||||
|
# Ein misslungenes Leeren ist kein Grund, das Lesen abzubrechen -
|
||||||
|
# dann steht eben der alte Wert da, so wie bisher auch.
|
||||||
|
_LOGGER.warning("Zwischenspeicher von %s nicht geleert: %s", name, fehler)
|
||||||
|
continue
|
||||||
|
geleert.append(name)
|
||||||
|
return geleert
|
||||||
|
|
||||||
|
|
||||||
|
async def lesen(k: Koordinator, frisch: bool = False) -> dict:
|
||||||
|
"""Holt die Konfiguration und gibt den fertigen Anzeigestand zurück.
|
||||||
|
|
||||||
|
Mit `frisch` wird vorher der Zwischenspeicher geleert (siehe
|
||||||
|
`_cache_leeren`). Die Werte, die danach zurueckkommen, koennen dadurch
|
||||||
|
fehlen - dann hat das Geraet sie noch nicht bestaetigt, und genau das
|
||||||
|
soll man sehen.
|
||||||
|
"""
|
||||||
token, geraet = await _geraet_und_token(k)
|
token, geraet = await _geraet_und_token(k)
|
||||||
eintraege = await _alle_einstellungen(k.hass, token, geraet)
|
eintraege = await _alle_einstellungen(k.hass, token, geraet)
|
||||||
return _stand(geraet, eintraege, wunsch_timeout(k))
|
geleert: list[str] = []
|
||||||
|
if frisch:
|
||||||
|
geleert = await _cache_leeren(k.hass, token, geraet, eintraege)
|
||||||
|
if geleert:
|
||||||
|
eintraege = await _alle_einstellungen(k.hass, token, geraet)
|
||||||
|
return _stand(geraet, eintraege, wunsch_timeout(k), geleert)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------- Schreiben
|
# --------------------------------------------------------------- Schreiben
|
||||||
@@ -545,7 +658,9 @@ async def jetzt_lesen(k: Koordinator) -> None:
|
|||||||
Antwort, auch auf ein "ging nicht".
|
Antwort, auch auf ein "ging nicht".
|
||||||
"""
|
"""
|
||||||
async with k.flespi_sperre:
|
async with k.flespi_sperre:
|
||||||
stand = await lesen(k)
|
# Auf Knopfdruck ein ECHTES Lesen: erst den Zwischenspeicher leeren,
|
||||||
|
# dann holen. Sonst kaeme nur zurueck, was flespi ohnehin schon hatte.
|
||||||
|
stand = await lesen(k, frisch=True)
|
||||||
await k.flespi_stand_setzen(stand)
|
await k.flespi_stand_setzen(stand)
|
||||||
# Ein frischer Stand kann eine Abweichung erst sichtbar machen - dann gehört
|
# Ein frischer Stand kann eine Abweichung erst sichtbar machen - dann gehört
|
||||||
# sie gleich behoben, statt bis zum nächsten Speichern zu warten.
|
# sie gleich behoben, statt bis zum nächsten Speichern zu warten.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":"2026.9.4.25","sha256":"7a53dd0854bfe487c6efc1fc91581e616dc3464bd31baa3356743883f9477331","bytes":324151,"gebaut":"2026-09-04T23:01:16Z"}
|
{"version":"2026.9.4.26","sha256":"0e65b78a949052abdcb6765a9a08e1526e67bec911657073d35f7158f51ba0e4","bytes":324250,"gebaut":"2026-09-04T23:10:56Z"}
|
||||||
Binary file not shown.
@@ -3500,6 +3500,19 @@ function csvDatei(name, kopf, zeilen) {
|
|||||||
a.download = name; document.body.appendChild(a); a.click(); a.remove();
|
a.download = name; document.body.appendChild(a); a.click(); a.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wann das Geraet einen Wert zuletzt bestaetigt hat - oder dass es das nie
|
||||||
|
getan hat. Steht dort, wo frueher nichts stand, wenn kein Urteil
|
||||||
|
moeglich ist: ein stiller Gleichstand mit einem veralteten Wert ist
|
||||||
|
schlimmer als gar kein Vergleich (04.09.2026). */
|
||||||
|
function dongleStand(w) {
|
||||||
|
if (!w.gemeldet_am) return "nie bestätigt";
|
||||||
|
const d = new Date(w.gemeldet_am);
|
||||||
|
if (!gueltigesDatum(d)) return "nie bestätigt";
|
||||||
|
const tage = Math.floor((Date.now() - d.getTime()) / 86400000);
|
||||||
|
const wann = dedat(d);
|
||||||
|
return tage >= 1 ? `bestätigt ${wann} (${tage} T. alt)` : `bestätigt ${wann}`;
|
||||||
|
}
|
||||||
|
|
||||||
function vEinst() {
|
function vEinst() {
|
||||||
return `
|
return `
|
||||||
<div class="tile"><span class="label">Fahrzeug einrichten</span>
|
<div class="tile"><span class="label">Fahrzeug einrichten</span>
|
||||||
@@ -3759,12 +3772,13 @@ function vEinst() {
|
|||||||
${DONGLE.stand && (DONGLE.stand.werte || []).length ? `<dl class="rows">
|
${DONGLE.stand && (DONGLE.stand.werte || []).length ? `<dl class="rows">
|
||||||
${DONGLE.stand.werte.map((w) => `<div class="row">
|
${DONGLE.stand.werte.map((w) => `<div class="row">
|
||||||
<dt>${esc(w.titel)} <small style="color:var(--fg3)">${esc(w.nummer)}</small></dt>
|
<dt>${esc(w.titel)} <small style="color:var(--fg3)">${esc(w.nummer)}</small></dt>
|
||||||
<dd${w.abweichung ? ` style="color:var(--warn)"` : ""}>${
|
<dd${w.abweichung === true ? ` style="color:var(--warn)"` : ""}>${
|
||||||
w.geraet === null || w.geraet === undefined ? "–" : esc(String(w.geraet))
|
w.geraet === null || w.geraet === undefined ? "–" : esc(String(w.geraet))
|
||||||
}${w.einheit ? " " + esc(w.einheit) : ""}${
|
}${w.einheit ? " " + esc(w.einheit) : ""}${
|
||||||
w.offen !== null && w.offen !== undefined
|
w.offen !== null && w.offen !== undefined
|
||||||
? ` <small>→ ${esc(String(w.offen))}, unterwegs</small>`
|
? ` <small>→ ${esc(String(w.offen))}, unterwegs</small>`
|
||||||
: w.abweichung ? ` <small>(wir: ${esc(String(w.unser))})</small>` : ""
|
: w.abweichung === true ? ` <small>(wir: ${esc(String(w.unser))})</small>`
|
||||||
|
: ` <small style="color:var(--fg3)">${dongleStand(w)}</small>`
|
||||||
}</dd>
|
}</dd>
|
||||||
</div>`).join("")}
|
</div>`).join("")}
|
||||||
</dl>` : ""}
|
</dl>` : ""}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "audi_dashboard",
|
"domain": "audi_dashboard",
|
||||||
"name": "Audi Dashboard",
|
"name": "Audi Dashboard",
|
||||||
"version": "2026.9.4.25",
|
"version": "2026.9.4.26",
|
||||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
|
|||||||
Reference in New Issue
Block a user