fix(station): Signiervariante aus Produkt-ID und Version bestimmen

Eine Entwicklungs-G95 meldete sich am 2026-09-15 mit einer Produkt-ID OHNE
"-dev", aber einer Version MIT "-dev". products.py leitete die Variante
ausschliesslich aus der Produkt-ID ab, die Lampe galt als Produktionsgeraet,
bekam das produktionssigniertes Image und lehnte es ab - ihr Bootloader prueft
gegen den Entwicklungsschluessel.

Die Variante ist damit keine Eigenschaft der Produkt-ID, sondern der Anfrage.
PRODUCTS bildet nur noch auf Geraetetyp und Anzeigename ab; beide Schreibweisen
der Produkt-ID fuehren auf denselben Eintrag. classify_variant() in
parse_request() wertet Produkt-ID UND Version aus: "-dev" an einer der beiden
Stellen bedeutet Entwicklungsgeraet, in der Version nur als ganzes Wort
("-devel"/"-dev2" zaehlen nicht).

Damit eine Fehlklassifikation nicht wieder erst an der Lampe auffaellt, fuehren
status.json (last_seen und jeder recent-Eintrag) und das Panel die Quelle der
Einstufung mit: "g95_cw (dev/Ver)", "dev/ID", "dev/ID+Ver" oder "prod".

Tests: die drei am Geraet mitgeschnittenen Anfragen plus die vierte denkbare
Kombination, die Wortgrenze in der Version, zwei Ende-zu-Ende-Tests im Server
(dev-Image statt prod-Image) und die Panel-Beschriftung.
This commit is contained in:
Michael Sauer
2026-09-15 00:24:29 +02:00
parent 6f0137908c
commit 6325ce073f
6 changed files with 216 additions and 33 deletions
+20
View File
@@ -33,6 +33,26 @@ def test_migration_shows_detected_lamp():
assert "13:04" in text
def test_migration_shows_where_the_dev_classification_came_from():
"""Der Hardwarebefund vom 2026-09-15: die Station lieferte ein falsch signiertes Image
aus, ohne dass am Panel etwas darauf hindeutete. Steht die Quelle der Einstufung
daneben, sieht der Bediener VOR der Freigabe, worauf sie beruht."""
def variant_line(last_seen):
state = {"mode": "migration", "phase": "listen", "served": 0, "recent": [],
"ssid": "qconnex-firmware", "last_seen": last_seen}
return [l for l in render.lines(state) if l.startswith("g95_cw (")][0]
base = {"product_name": "Klyqa G95 Vintage CW/WW", "target": "g95_cw",
"lamp_version": "V1.1.1.2357-dev"}
assert variant_line(dict(base, variant="dev", variant_source="version")) == "g95_cw (dev/Ver)"
assert variant_line(dict(base, variant="dev", variant_source="product_id")) == "g95_cw (dev/ID)"
assert variant_line(dict(base, variant="dev", variant_source="both")) == "g95_cw (dev/ID+Ver)"
# prod hat keine Quelle - da gibt es nirgends ein "-dev", das man anzeigen koennte.
assert variant_line(dict(base, variant="prod", variant_source="none")) == "g95_cw (prod)"
# Eine aeltere Serverfassung ohne das Feld bleibt darstellbar.
assert variant_line(dict(base, variant="dev")) == "g95_cw (dev)"
def test_recovery_mode_without_traffic():
lines = render.lines({"mode": "recovery", "ssid": "ninlight-recovery",
"served": 0, "recent": []})