Netzwerk-Labor · Doku đŸ§± Architektur – Netzwerk-Labor Zum Spiel Online

đŸ§± Architektur – Netzwerk-Labor

âŹ†ïž Netzwerk-Labor · Spezifikation: Konzept – Netzwerk-Labor

Diese Notiz ist der Vertrag zwischen den Bausteinen. Wer ein Modul baut, hÀlt sich an die Namen, Datenformen und Aufrufe hier. Wer etwas Àndern muss, Àndert es hier zuerst.

1 · GrundsÀtze

SchichtObjektErste DateiDOM?Zeit/Zufall?
KernIP, Bus, Zufall, store, heute, plusTage, tageZwischen, tief, esckern/basis.js, kern/netz.jsneinnur jetzt() in basis
ModellModellmodell/geraete.jsneinnein
SimulationSimsim/engine.jsnienie (virtuelle Zeit, Seed)
KonsoleCLIcli/parser.jsnienie
SpielSpielspiel/zustand.jsneinnur ĂŒber jetzt()
DatenDATENdaten/lehrtexte.jsneinnein
PlattformPlattformplattform/plattform.jsjaja
OberflÀcheUIui/dom.jsjaja
LernmotorLSpielhalle lernmotor.jsneinDatum

2 · Build und Ordner

Netzwerk-Labor/
  bauen.py             → web/index.html (Einzeldatei + web/schriften/), --paket → Browser-ZIP (Nebenprodukt)
  src/seite.html       HĂŒlle mit /*STIL*/, /*SKRIPTE*/, <!--SCHRIFTEN-->
  src/stil/*.css       Reihenfolge in bauen.py (STILE)
  src/**/*.js          Reihenfolge in bauen.py (MODULE)
  shell/src-tauri/     Tauri-Projekt (frontendDist = ../../web)
  tests/               harness.js, run.js (Node), tests.html (Browser), *.test.js
  tools/               messen.ps1, linux-bauen.sh, cdp.py (Test im echten Programm), klassen.py (Klassen ↔ CSS), rauch.py (Rauchtest der OberflĂ€che), kiosk.py (VorfĂŒhr-Fassung)
  web/                 Build-Ausgabe

Neue Datei? In bauen.py in MODULE (bzw. STILE) an der richtigen Stelle eintragen, und – falls headless-fĂ€hig – in tests/run.js in HEADLESS. Eine Datei, die nicht in MODULE steht, existiert fĂŒr das Programm nicht.

Tests: sh tools/test.sh [filter] (Node 24 portabel; frĂŒher WSL). Testdateien nutzen pruefe(name, fn) und erwarte-Hilfen aus tests/harness.js. Die Tests laden ui/ nicht – dafĂŒr gibt es sh tools/test.sh --rauch: Node-Tests, tools/klassen.py (muss 0 melden) und tools/rauch.py (jede Ansicht in 1366/960/720 px in Edge headless: Hauptaktion sichtbar und anklickbar, kein waagerechter Überlauf, keine JS-Fehler; --exe prĂŒft das echte Programm).

3 · Kern (kern/basis.js, kern/netz.js)

Nachtrag 31.10.2026 (KRN-01, aus dem Code gemessen). VollstÀndige Kern-OberflÀche:

4 · Modell (Konfiguration ist Daten)

Ein Netz ist reines JSON:

{ v:1,
  geraete: { [id]: Geraet },
  kabel:   [ { id:"k1", a:{geraet:"pc1", port:"eth0"}, b:{geraet:"sw1", port:"Fa0/1"} } ],
  zustand: { _uhr:0, [id]: Laufzeit }      // Laufzeit der Simulation (ARP, MAC-Tabellen 
), siehe § 5.4
}

Geraet

{ id:"r1", typ:"pc"|"server"|"switch"|"router"|"firewall"|"internet"|"nas",
  skin:null|"laptop"|"drucker"|"kasse"|"tablet",      // nur Optik bei pc
  name:"R1", x:400, y:200, an:true,
  hw:{ macs:{ "Gi0/0":"00:1a:2b:3c:4d:01", 
 } },     // je physischem Port, fest
  running: Konfig, startup: Konfig|null,              // null = Werkszustand (kein startup-config)
  flash: { vlans:{ "1":{name:"default"}, "10":{name:"Verwaltung"} } }   // nur switch: vlan.dat
}

Ports je Typ (Modell.PORTS): pc/server ["eth0"] · nas ["eth0","eth1"] · switch Fa0/1
Fa0/24, Gi0/1, Gi0/2 · router Gi0/0, Gi0/1, Gi0/2 · firewall Gi0/0
Gi0/3 · internet wan0
wan3. Subinterfaces (Gi0/0.10) sind nur Konfiguration, keine Ports.

Konfig je Typ (Werkszustand = Modell.werkszustand(typ)):

// pc, server, nas
{ hostname:"PC1",
  if:{ eth0:{ an:true, dhcp:false, ip:"", maske:"", gw:"", dns:"" } },
  dienste:{ http:{an:true}, https:{an:false}, dns:{an:false, eintraege:[{name:"intranet.salon.local", ip:"192.168.1.5"}]},
            dhcp:{an:false, pools:[{name, netz, maske, gw, dns, start, anzahl}]}, datei:{an:false}, ssh:{an:false}, druck:{an:false} } }
// Dienst → Port: http tcp/80, https tcp/443, dns udp/53, dhcp udp/67, datei tcp/445, ssh tcp/22, druck tcp/9100 (Modell.DIENSTPORTS)

// switch  (VLANs stehen in geraet.flash.vlans, NICHT in der Konfig – wie vlan.dat)
{ hostname:"Switch", enableSecret:null, banner:"",
  ports:{ "Fa0/1":{ modus:"access"|"trunk", accessVlan:1, trunkErlaubt:"all"|[1,10,20], nativeVlan:1, shutdown:false, beschreibung:"",
                    portSecurity:null|{max:1, verstoss:"shutdown", macs:[]} }, 
 },
  svi:{ "1":{ ip:"", maske:"", shutdown:true } },
  defaultGateway:"" }

// router
{ hostname:"Router", enableSecret:null, banner:"",
  if:{ "Gi0/0":{ ip:"", maske:"", shutdown:true, beschreibung:"", nat:null|"inside"|"outside", aclIn:null, aclOut:null, helper:[] },
       "Gi0/0.10":{ vlan:10, nativ:false, ip:"", maske:"", shutdown:false, beschreibung:"", nat:null, aclIn:null, aclOut:null, helper:[] } },
  routen:[ { netz:"10.0.2.0", maske:"255.255.255.0", nh:"10.0.1.2"|null, aus:"Gi0/1"|null, ad:1 } ],
  acls:{ "10":{ typ:"standard", benannt:false, regeln:[Regel] }, "GAST":{ typ:"erweitert", benannt:true, regeln:[Regel] } },
  nat:{ statisch:[{ innen:"192.168.1.10", aussen:"203.0.113.10", proto:null|"tcp"|"udp", innenPort:null, aussenPort:null }],
        dynamisch:[{ acl:"1", aus:"Gi0/1", overload:true }] },
  dhcp:{ ausgeschlossen:[{von:"192.168.10.1", bis:"192.168.10.9"}], pools:[{ name:"LAN", netz:"192.168.10.0", maske:"255.255.255.0", gw:"192.168.10.1", dns:"" }] } }

// Regel (ACL)
{ aktion:"permit"|"deny", proto:"ip"|"tcp"|"udp"|"icmp",
  quelle:{ip:"192.168.1.0", wc:"0.0.0.255"}, ziel:{ip:"0.0.0.0", wc:"255.255.255.255"},   // any = 0.0.0.0/255.255.255.255, host = wc 0.0.0.0
  zielPort:null|{op:"eq"|"neq"|"gt"|"lt"|"range", ports:[80]}, quellPort:null|{
}, icmpTyp:null|"echo"|"echo-reply" }
// Standard-ACL: nur quelle, proto "ip". Implizites deny any am Ende.

// firewall (herstellerneutral)
{ hostname:"FW1",
  if:{ "Gi0/0":{ ip:"", maske:"", zone:"innen"|"aussen"|"dmz"|null, shutdown:false } },
  routen:[
wie router
],
  regeln:[ { id:"r1", von:"innen", nach:"aussen", proto:"ip"|"tcp"|"udp"|"icmp", quelle:"any"|"192.168.1.0/24", ziel:"any"|"cidr", port:null|443, aktion:"erlauben"|"verwerfen", aktiv:true, text:"" } ],
  nat:{ quellNat:[{von:"innen", nach:"aussen"}], weiterleitung:[{ proto:"tcp", aussenPort:443, ziel:"172.16.0.10", zielPort:443 }] } }
// Erste passende Regel gewinnt, am Ende implizit verwerfen, zustandsbehaftet (RĂŒckverkehr erlaubt).

// internet (Kulisse, nicht konfigurierbar im Spiel)
{ hostname:"Internet",
  if:{ wan0:{ ip:"203.0.113.1", maske:"255.255.255.252" }, 
 },
  routen:[ {netz:"203.0.113.16", maske:"255.255.255.240", nh:"203.0.113.2"} ],
  server:[ { name:"www.beispiel.de", ip:"198.51.100.10", dienste:["http","https"] }, { name:"dns.beispiel.de", ip:"198.51.100.53", dienste:["dns"] } ] }
// Verwirft Pakete mit privater Quelladresse (Grund NAT_MISSING).

Modell-API (modell/*.js): Modell.neu(), Modell.geraet(netz, typ, {id?, name?, x, y, skin?}) (legt an, vergibt MACs), Modell.entfernen(netz, id), Modell.ports(geraet), Modell.verbinden(netz, a, b) (a/b = {geraet, port?}; ohne Port → nĂ€chster freier passender) → Kabel oder {fehler}, Modell.trennen(netz, kabelId), Modell.kabelAn(netz, id, port) → {kabel, gegen:{geraet,port}}|null, Modell.freierPort(netz, id), Modell.linkOben(netz, kabel) → {oben, grund}, Modell.konfig(geraet) (= running), Modell.setzen(netz, id, pfad, wert) (Pfad wie "if.eth0.gw"), Modell.speichern(geraet) (running → startup), Modell.neustart(netz, id) (running := tief(startup) oder Werkszustand, Laufzeit leeren), Modell.kopie(netz), Modell.pruefen(netz) → Warnungen [{geraet, feld, text, code}] (Live-PrĂŒfung: „10.0.0.5/24 liegt nicht im Netz von Gateway 10.0.1.1“), Modell.gleich(a, b).

RĂŒckgĂ€ngig (modell/verlauf.js): Modell.verlauf(netz) → {aendern(beschreibung, fn), zurueck(), vor(), kannZurueck, kannVor, liste}. Schnappschuss-basiert (Netz ist klein), höchstens 100 Schritte. Jede UI- und KonsolenĂ€nderung geht ĂŒber aendern. Danach Bus.senden("netz-geaendert", {netz, beschreibung}).

Nachtrag 31.10.2026 (KRN-01, aus dem Code gemessen).

5 · Simulation (Sim)

5.1 Aufrufe (verÀndern netz.zustand, nie die Konfig)

Sim.ping(netz, vonId, ziel, {anzahl:4, ttl:null, quelle:null})   // ziel = IP oder GerÀtename (DNS)
  → { ok, antworten:[{ok, grund|null, rtt, ttl, von}], trace, text }
Sim.traceroute(netz, vonId, zielIp, {maxHops:30})  → { hops:[{nr, ip|null, grund|null}], ok, trace }
Sim.tcp(netz, vonId, ziel, port)                  → { ok, grund, trace }       // Handshake SYN, SYN/ACK, ACK (+FIN)
Sim.http(netz, vonId, url)                        → { ok, status, grund, trace } // DNS + TCP 80/443 + GET
Sim.dns(netz, vonId, name)                        → { ok, ip, grund, trace }
Sim.dhcp(netz, vonId, port="eth0")                → { ok, lease|null, grund, trace } // DORA; bei Fehlschlag APIPA
Sim.adresse(netz, id, port)                       → { ip, maske, gw, dns, quelle:"statisch"|"dhcp"|"apipa"|"keine" }
Sim.pruefeZiel(netz, ziel)                         → { ok, grund, trace, text }   // Ziel siehe § 7.1; nutzt eine Kopie
Sim.GRUENDE[code] → { titel, skill, schicht }      // Lehrtexte in DATEN.lehrtexte[code]

Hosts mit dhcp:true und ohne gĂŒltige Lease holen sich zuerst per DHCP eine Adresse (sichtbar in derselben Trace). Scheitert das: APIPA 169.254.x.x und Grund DHCP_NO_OFFER.

5.2 Trace (reine Daten, die UI spielt nur ab)

Trace = { start, ende, ereignisse:[Ereignis], abbruch:null|"STORM", zusammenfassung:"
" }
Ereignis = {
  n:1, t:0.8,                       // laufende Nr., virtuelle Zeit in ms
  art:"senden"|"empfangen"|"weiterleiten"|"fluten"|"verwerfen"|"antworten"|"lernen"|"info",
  geraet:"sw1", port:"Fa0/1"|null,
  nach:{geraet, port}|null,         // bei "senden": Gegenstelle des Kabels
  frame: Frame,                     // Kopie, unverÀnderlich
  proto:"ARP"|"ICMP"|"TCP"|"UDP"|"DHCP"|"DNS"|"HTTP",
  grund:null|"ARP_NO_REPLY"|
,      // bei verwerfen/info
  warnung:true,                     // optional: nur bei Hinweisen (z. B. CDP NATIVE_MISMATCH) – keine Ursache; _erster/stelle/erklaere werten solche Ereignisse NICHT als entscheidende Stelle
  harmlos:true,                     // optional: normales/gewolltes Verwerfen ohne Grund (z. B. IOS verwirft Paket wĂ€hrend ARP, NIC verwirft fremde MAC) – kein Fehler; _erster/stelle/erklaere werten es nicht als Ursache
  text:"SW1 kennt MAC 00:1a
 noch nicht und flutet in VLAN 10"   // kurzer Klartext
}
Frame = {
  eth:{ src, dst, typ:"ARP"|"IPv4", vlan:null|10 },            // vlan = 802.1Q-Tag (nur auf Trunk sichtbar)
  arp:null|{ op:"request"|"reply", senderMac, senderIp, targetMac, targetIp },
  ip:null|{ src, dst, ttl, proto:"ICMP"|"TCP"|"UDP", id },
  icmp:null|{ typ:"echo-request"|"echo-reply"|"unreachable"|"time-exceeded", code:null|"net"|"host"|"port"|"admin", seq },
  udp:null|{ src, dst }, tcp:null|{ src, dst, flags:["SYN","ACK"], seq, ack },
  app:null|{ proto:"DHCP"|"DNS"|"HTTP", info:"DHCP Discover", felder:{
} }
}

Ereignis-Budget je Lauf 5000; gerissen → abbruch:"STORM", Grund STORM. Warnungen (warnung:true) sind Hinweise und dĂŒrfen die Diagnose nicht verfĂ€lschen: Sim._erster, Sim.stelle und Sim.erklaere ĂŒberspringen sie (SIM-02, 31.10.2026); die Warnung bleibt vollstĂ€ndig in der Trace. Virtuelle Zeit: 1 ms je Kabel, Timeout Ping 2000 ms (IOS) bzw. 4000 ms (Windows). Verwerfen ohne Fehlergrund trĂ€gt harmlos:true (normal/gewollt). Jedes Verwerfen hat damit grund oder harmlos (SIM-05/06); die 21 bisher unbegrĂŒndeten Stellen sind so klassifiziert.

5.3 GrĂŒnde (Codes)

LINK_DOWN, PORT_SHUTDOWN, NO_IP, ARP_NO_REPLY, DROP_VLAN, TRUNK_NOT_ALLOWED, NATIVE_MISMATCH, NO_GATEWAY, GW_WRONG_SUBNET, GW_UNREACHABLE, WRONG_MASK, NO_ROUTE, NO_RETURN_ROUTE, TTL_EXPIRED, ACL_DENY, FW_DENY, NAT_MISSING, DUP_IP, DHCP_NO_OFFER, DHCP_POOL_EMPTY, DNS_FAIL, DNS_NO_SERVER, PORT_CLOSED, SERVICE_OFF, STORM, PORTSEC_VIOLATION, DEVICE_OFF, HOST_UNREACHABLE, TIMEOUT

Sim.GRUENDE[code] = {titel, skill:"lab.*", schicht:1..7}. Lehrtexte in drei Tiefen stehen in DATEN.lehrtexte[code] = {E, AP1, AP2, quelle} (E erklĂ€rt, AP1 knapp, AP2 nur, was das GerĂ€t melden wĂŒrde).

5.4 Laufzeit (netz.zustand[id])

{ arp:{ [ip]:{mac, bis} }, mac:{ [vlan]:{ [mac]:{port, bis} } }, leases:{ [ip]:{mac, bis} }, dhcp:{ [port]:{ip, maske, gw, dns, server, bis} }, nat:[
], fw:[
] } plus zustand._uhr (virtuelle Zeit, lĂ€uft ĂŒber LĂ€ufe weiter). MAC-Alterung 300 s, ARP 14400 s (Router) bzw. 120 s (Host, vereinfacht). Link down leert die MAC-EintrĂ€ge dieses Ports; IP-Änderung leert den ARP-Cache des GerĂ€ts (Modell.setzen erledigt das).

6 · Konsole (CLI)

CLI.sitzung(netz, geraetId, {verlauf?}) → Sitzung      // IOS fĂŒr switch/router/firewall(GrundzĂŒge), Windows-Terminal fĂŒr pc/server/nas
CLI.eingabe(sitzung, zeile)  → { ausgabe:"
", prompt:"R1(config-if)#", geaendert:bool, befehl:"interface gigabitEthernet0/0" }
CLI.tab(sitzung, zeile)      → { zeile, vorschlaege:[] }
CLI.prompt(sitzung)          → "R1#"
CLI.runningConfig(geraet)    → Text (show running-config)
CLI.entspricht(geraet, pfad, wert) → "interface Gi0/0\n ip address 10.0.0.1 255.255.255.0" | null   // „entspricht:“-Zeile der GUI
CLI.anwenden(netz, geraetId, befehle) → { ok, fehler:[] }   // mehrzeiliger Block ab privilegiertem Modus, fĂŒr Lösungen und Tests
CLI.vorschlag(sitzung)       → nĂ€chster sinnvoller Befehl (Einstieg) | null

Änderungen der Konsole laufen ĂŒber Modell.setzen/Verlauf (eine Quelle der Wahrheit). ping/traceroute/tracert/nslookup/ipconfig /renew rufen Sim und formatieren wie IOS bzw. Windows (.!!!!, „Antwort von 
“).

6.1 running-config — Anzeige und Wiedereinspielen (verbindlich, 31.10.2026)

CLI.runningConfig(geraet) liefert den Text von der ersten !-Zeile bis einschließlich end (ohne die zwei Kopfzeilen, die show running-config davor setzt). Geltungsbereich: Router und Switch. Firewall: herstellerneutrale Anzeige, ausdrĂŒcklich nicht wiedereinspielbar und im Text als Kommentar gekennzeichnet. Hosts (pc/server/nas): null.

Wiedereinspiel-Invariante. Jede Zeile von CLI.runningConfig ist ĂŒber CLI.anwenden(netz, id, "configure terminal\n" + text) fehlerfrei einlesbar — ok:true, fehler:[] — inklusive !-Kommentaren und abschließendem end. Die Konsole betritt keinen Konfigurationsmodus von selbst; der Wiedergabeweg beginnt ausdrĂŒcklich mit configure terminal (IOS-Weg). Nach dem Einspielen auf ein GerĂ€t desselben Typs gilt: (a) CLI.runningConfig(kopie) === text (zeichengleich, damit entspricht:- und Show-Ausgaben stabil bleiben), (b) die Modell-Kernfelder sind tiefgleich: running.if, running.ports, running.svi, running.dhcp (Pools samt leaseS, domain, reservierungen), running.acls, running.routen, running.nat, running.snooping, running.hostname, running.lines, running.banner, running.benutzer.

VLAN-Datenbank. flash.vlans gehört wie auf IOS nicht zur running-config. Sie wird separat angelegt (vlan 20, name 
), steht in show flash: als vlan.dat und ĂŒberlebt write erase; weg ist sie erst mit delete flash:vlan.dat.

Anzeigeform (verbindlich). Die running-config zeigt nur Zeilen, die das GerÀt selbst wieder annimmt; reine Komfort-Defaults ohne Befehl entfallen: version 
, service timestamps 
, boot-start-marker, boot-end-marker, no aaa new-model, system mtu routing 1500, spanning-tree mode pvst, spanning-tree extend system-id, vlan internal allocation policy ascending, duplex auto, speed auto, ip forward-protocol nd, no service pad, line aux 0. end bleibt als Blockende zulÀssig.

DHCP-Pools vollstĂ€ndig: je Pool ip dhcp pool NAME, network, default-router, dns-server, lease 
, domain-name 
; je Reservierung host NAME, hardware-address 
, ip 
, danach exit (verlĂ€sst den Reservierungsmodus; nötig, weil aus (dhcp-config-host) sonst kein globales Kommando mehr erreichbar ist).

SSH (D3). ip ssh version 2 wird nicht als Befehl ausgegeben (der Parser kennt ihn heute nicht; Nachbau CLI-05/3.1). Ist ein RSA-SchlĂŒssel gesetzt, erscheint stattdessen die Kommentarzeile: ! SSH aktiv – „ip ssh version 2“ folgt in 3.1 (CLI-05). Das crypto key generate rsa ist wie auf IOS nicht Teil der running-config. Folge: der bisherige Assert in tests/cli-ios-ssh.test.js („ip ssh version 2“ im Text) wird auf diese ehrliche Kennzeichnung umgestellt; der SSH-Zustand bleibt ĂŒber running.sshSchluessel geprĂŒft (keine AbschwĂ€chung).

DHCP-Snooping am Switch (CLI-03, 31.10.2026). running.snooping = {an: bool, vertraut: [Port, 
]} ist Teil der laufenden Konfiguration. Die running-config zeigt ip dhcp snooping genau dann, wenn an gilt, und je vertrautem Port ip dhcp snooping trust im interface-Block. Beide Zeilen sind ĂŒber configure terminal wieder einlesbar; die trust-Zeile Ă€ndert ausschließlich vertraut, nie den globalen Schalter. ip dhcp snooping trust ist nur in (config-if) und (config-if-range) zulĂ€ssig; im globalen Konfigurationsmodus antwortet die Konsole mit Fehler und Hinweis auf den Schnittstellenmodus. no ip dhcp snooping trust entfernt den Port, no ip dhcp snooping schaltet global aus und lĂ€sst die Portliste stehen.

show ip dhcp snooping (nur Switch) zeigt „Switch DHCP snooping is enabled|disabled", „DHCP snooping is configured on following VLANs: 1", die Tabelle der vertrauten Ports („Interface"/„Trusted") und die Zeile „DHCP snooping verworfen (seit dem letzten Neustart)" je Port. Die ZĂ€hler stammen aus dem Laufzeitspeicher netz.zustand[id].snooping[port].verworfen (src/sim/switch.js:64-69, § 5.4) und sind ausdrĂŒcklich Laufzeitdaten, kein Konfigurationszustand — deshalb stehen sie nicht in der running-config. Bei ausgeschaltetem Snooping nennt die Ausgabe den Befehl zum Einschalten. Ehrliche Grenze: Das Modell bildet Snooping nur ĂŒber das Port-Vertrauen ab, nicht ĂŒber eine VLAN-Zuordnung; echte Cisco-Hardware wurde nicht geprĂŒft. Die Roundtrip-Invariante aus § 6.1 gilt unverĂ€ndert.

6.2 Einleseformen und Grenzen (verbindlich, 31.10.2026)

Geheimnisse. Typ-PrĂ€fixe 0 (Klartext), 5 (MD5-artig), 7 (verschleiert) werden beim Einlesen akzeptiert und unverĂ€ndert wieder angezeigt — fĂŒr enable secret, username 
 secret|password und password in line. Gespeichert wird die eingegebene Form; reine Klartext-Eingaben erscheinen weiterhin in der bisherigen Anzeigeform (Typ 5 fĂŒr secret, Typ 0/7 fĂŒr password). Erneutes Einlesen erzeugt dieselbe Anzeige.

no ip domain lookup — Anzeige und Befehl sind deckungsgleich; die running-config schreibt die Tokenform des Befehls (no ip domain-lookup), nicht die IOS-Kosmetik mit Leerzeichen.

CLI.anwenden(netz, id, text, {verlauf?}) — Vertrag unverĂ€ndert: beginnt im privilegierten Modus (#), fĂŒhrt !-Kommentarzeilen und Leerzeilen aus, beantwortet nur automatische RĂŒckfragen (Enter/confirm) selbst, liefert {ok, fehler:[{nr, zeile, meldung}]}. Ein Block, der einen Konfigurationsmodus braucht, beginnt mit configure terminal.

Nicht wiedereinspielbar (Grenze, ehrlich): Firewall-Konfiguration (herstellerneutral) und alles, was nur im Inspektor konfigurierbar ist. Hosts haben keine IOS-running-config. Die show running-config-Kopfzeilen gehören nicht zum Vertragstext.

Vertrags-Test: tests/cli-runningconfig.test.js (Roundtrip Router/Switch/Startup, Zustandsvergleich, Phantomliste, absichtlich verlorenes Feld → rot).

7 · Spiel (Spiel)

7.1 Ticket (Daten in daten/tickets-*.js → DATEN.tickets)

{ id:"salon-01", art:"stoerung"|"projekt"|"wartung"|"mini", stufe:"E"|"AP1"|"AP2", karriere:1,
  kunde:"salon", titel:"Drucker nicht erreichbar", briefing:"Hallo, 
", symptom:"Kasse druckt nicht",
  skills:["lab.gateway"],
  netz:(z) => Netz,                  // Fabrik; z = Zufall(seed) fĂŒr Varianten; liefert das GESUNDE oder Start-Netz
  fehler:[{ injektor:"gw-falsch", auf:"pc-kasse", param:{} }],     // optional, auf das Netz angewendet
  ziele:[ { typ:"erreichbar", von:"pc-kasse", nach:"drucker", proto:"icmp"|"tcp"|"http"|"dns", port:null, text:"Kasse erreicht Drucker" },
          { typ:"blockiert",  von:"gast", nach:"server", proto:"tcp", port:445, text:"GĂ€ste kommen nicht an die Dateifreigabe" },
          { typ:"dhcp", von:"pc2", text:"PC2 bekommt per DHCP eine Adresse" },
          { typ:"konfig", geraet:"sw1", pfad:"ports.Fa0/5.accessVlan", wert:20, text:"
" },
          { typ:"gespeichert", geraet:"r1", text:"Konfiguration gesichert" } ],
  hilfen:{ frage:["
"], bereich:[{geraet:"pc-kasse"}], konkret:["
"] },
  loesung:[ { geraet:"pc-kasse", setzen:{ "if.eth0.gw":"192.168.1.1" }, text:"Gateway korrigieren" },
            { geraet:"r1", cli:"conf t\ninterface Gi0/1\nno shutdown\nend", text:"Schnittstelle einschalten" } ],
  erklaerung:"
", quelle:"Network – Lernfassung", lohn:{ euro:40, ruf:1 } }

Pflicht (Test): Jede Lösung erfĂŒllt alle Ziele; das Start-Netz verletzt mindestens ein Ziel, und zwar mit dem erwarteten Grund.

7.2 Spielstand (store "labor", v:3)

Der Lebenszyklus der Wiederholungs-Tickets im Postfach steht in § 7.5.

{ v, euro, ruf, stufe, kunden:{[id]:{vertrag, ampel, seit, sterne:[]}}, postfach:[TicketInstanz], aktiv:iid|null, erledigt:[{id, sterne, tag, hilfe}], playbooks:{slots, aktiv:[skill]}, tag:{
Arbeitstag
}, zuletzt:ms, einstieg:{
} } (Die Liste ist ein Auszug; weitere Felder u. a. naechsteIid, buch, angebot, dex, tagesraetsel, tagebuch. Die Einstellungen liegen NICHT hier, sondern unter dem eigenen Store-SchlĂŒssel "einst" — st.einst gibt es nicht.)

TicketInstanz = { iid, ticketId, seed, netz, hilfeStufe, hilfen:[], start, frist|null, quelle:"postfach"|"wartung"|"wiederholung"|"generiert"|"notfall"|"raetsel"|"pruefung"|"folge", vielfalt?, kuratiert?, kuratiertId?, klingelt? }

Onboarding, nachgetragen 06.10.2026 (spiel/erstestunde.js): st.einstieg = { fertig, coach:{}, begruessung?:"auftrag"|"umsehen", abschluss?:true }. begruessung setzt die Wahl der BegrĂŒĂŸungskarte — genau einmal, und nur in einem frischen Stand (!fertig und kein erledigter Auftrag). abschluss markiert die gezeigte Abschluss-Station und verlangt begruessung und fertig. Fehlt einstieg ganz oder steht fertig: true, sieht ein Altstand nichts davon (die BegrĂŒĂŸung entfĂ€llt, der Abschluss ebenso).

Auftragsvielfalt (neu 06.10.2026, spiel/postfach.js · spiel/generator.js · daten/basis.js)

Vorher war jeder handgeschriebene Auftrag starr: def.netz ignorierte den Instanz-Seed, das Netz war bei Seed 1 und Seed 987654321 in 46 von 46 FĂ€llen byte-gleich. Jetzt:

7.3 Spiel-API (Auswahl)

Spiel.laden(), Spiel.st (Zustand), Spiel.postfach(), Spiel.oeffnen(iid), Spiel.abnahme(inst) → [{ziel, ok, grund, trace}] + Sterne, Spiel.hilfe(inst) → nĂ€chste Stufe, Spiel.vorfuehren(inst) → Lösungsschritte, Spiel.tick(ms) (Idle: Wartung, Einkommen, Ampeln), Spiel.offlineBericht(), Spiel.mini() → Mini-Ticket, Spiel.generiere(skill, seed) (Injektor-Generator), Spiel.INJEKTOREN.

Bus-Ereignisse: netz-geaendert, ticket-neu, ticket-geloest, zustand-geaendert, trace, modus (leiste/voll), hilfe.

7.4 Speichern — verbindlicher Vertrag (06.10.2026)

PunktVertrag
SchlĂŒssellabor (Spielstand), einst (Einstellungen), sandbox (freies Labor), labor-sicherung (rollierende Zweitsicherung), klassenraum (Klassenraum-Sitzungen, fassung: 2 seit 3.0.0 — Nachtrag 09.10.2026, § 12.1). Geschrieben wird der ganze Speicher als ein JSON unter dem einen SchlĂŒssel netzwerk-labor — im Browser und in der Android-Fassung in localStorage, im Desktop-Programm ĂŒber Tauri in spielstand.json. Neue SchlĂŒssel brauchen keine Änderung an kern/basis.js — die Speicher-Map ist frei
Entprellunggenau eine: SPEICHER.entprellung = 1500 ms (kern/basis.js). Spiel.speichern() markiert nur „schmutzig"; geschrieben wird 1500 ms nach der letzten Änderung. Spiel.AUTOSPEICHERN_MS = 0 — die frĂŒhere zweite Schicht (400 ms in spiel/ticket.js) ist entfallen
Sofort schreibenstore.sofort(); im Spiel Spiel.sofortSpeichern() (RĂŒckgabe immer erfĂŒllt — ein Fehler ist bereits gemeldet). Aufrufer: Auftragsabschluss spiel/abnahme.js (bestanden und nicht bestanden), Ticket öffnen spiel/ticket.js, KĂ€ufe spiel/wirtschaft.js · spiel/werkzeuge.js · spiel/playbooks.js, Meilensteine spiel/karriere.js (Aufstieg, Fest gesehen), App in den Hintergrund ui/start.js (pagehide, beforeunload, visibilitychange → hidden)
Kennzeichenstore.stand() → {art:"schreibt"\|"gesichert"\|"fehler", fehler, zeit}. Der Bus meldet jede echte Änderung als speicher-stand, ein Fehler zusĂ€tzlich als speicher-fehler. Die Kopfzeile zeigt daraus „
 sichert / ✓ gesichert / ⚠ nicht gesichert" (ui/app.js, role="status", kein Klickziel, keine Animation; unter 380 px Breite Kurzform)
FehlerEin gescheiterter Schreibvorgang lĂ€sst schmutzig stehen (der nĂ€chste Versuch nimmt den Stand mit), fĂŒllt SPEICHER.fehler und meldet ihn ĂŒber den Bus; der Entpreller fĂ€ngt seine eigene Ablehnung ab. Vorher blieb ein voller Speicher stumm
SicherungSpiel.SICHERUNG = "labor-sicherung", Inhalt {v, zeit, stand}. Angelegt vor einer Migration (v < Spiel.VERSION) und vor einem Import (Spiel.sicherungAnlegen(roh, true)); eine frische Sicherung bleibt Spiel.SICHERUNG_FRISCH_MS = 60 s unangetastet, damit die Migration nach dem Neuladen die Import-Sicherung nicht ersetzt
ImportSpiel.importPruefen(d): fremdes Format (format !== "netzwerk-labor" oder fehlendes speicher) und zu neue StĂ€nde (labor.v > Spiel.VERSION) werden abgewiesen; alte StĂ€nde und StĂ€nde ganz ohne v-Feld sind erlaubt — die Migration holt sie herein. GeprĂŒft wird die Schemaversion, nicht die Programmversion der Datei
Nicht im StandLaufzeit je Instanz (Spiel._lz: Verlauf, Startnetz, Ziele, letzteArbeit) und alles, was ui/ nur anzeigt. Ein Neuladen holt den Stand zurĂŒck, nicht die Sitzung

Gemessen (06.10.2026, Edge headless, Einzeldatei-Fassung, 1366×768): Spiel.speichern() 2010 → 1512 ms bis zum Schreibvorgang, Netzwechsel 2415 → 1515 ms, Auftragsabschluss 2436 → 6,7 ms (Gegenprobe des PrĂŒfers: 22 ms), Kauf 3,8 ms (PrĂŒfer: 24 ms), Verstecken der Seite 0 → 1 Schreibvorgang. Spielstand: 26.554 B beim Start, 27.420 B nach einem Abschluss, 102.799 B bei 30 Postfach-Instanzen. Grenze: ein Prozess-Tod ohne hidden-Wechsel verliert die letzten ≀ 1500 ms.

Sicherung wiederherstellen (3.0.0, SPL-02). Spiel.sicherung() liefert {v, zeit, stand} aus dem SchlĂŒssel labor-sicherung oder null. Neu ist der RĂŒckweg:

Spielstand-Migration 2.9.0 → 3.0.0 (neu 3.0.0, SPL-10). Die Schemaversion des Spielstands bleibt labor.v = 3: ein echter 2.9.0-Stand trĂ€gt v:3 und wird unverĂ€ndert ĂŒbernommen (unbekannte Felder bleiben erhalten). Ältere StĂ€nde (v < 3 oder ohne v) migriert Spiel.laden(); vor der Migration legt laden die Sicherung labor-sicherung an (bestehendes Verhalten, siehe oben), fehlende Pflichtfelder ergĂ€nzen migrieren und die Spiel.ergaenzer-Kette, unbrauchbare EintrĂ€ge (z. B. postfach/kunden) werden verworfen statt zu werfen. Ein ganzer 2.9.0-Export (format:"netzwerk-labor", speicher) wird ĂŒber store.initialisieren + Spiel.laden() ĂŒbernommen; ein darin enthaltener klassenraum-Altstand (fassung 1) wird beim Lesen nach fassung 2 migriert (KLR-01a). Eine im Export enthaltene labor-sicherung bleibt Sicherung und wird nicht der laufende Stand. FehlerfĂ€lle: labor.v > Spiel.VERSION weist Spiel.importPruefen ab; ein Stand ganz ohne v gilt als v:0 und wird migriert (kein stiller Verlust). Abnahme: tests/spielstand-migration.test.js mit ≄ 8 Zusicherungen (v:3 unverĂ€ndert, v:1→v:3 mit Sicherung, kaputte Felder repariert, unbekannte Felder bleiben, Klassenraum fassung 1→2, zu neuer Stand abgewiesen) + Gegenprobe: fehlt in einer Kopie ein Pflichtfeld, wird die PrĂŒfung rot.

7.5 Postfach-Lebenszyklus (3.0.0, SPL-01) — verbindlicher Vertrag (31.10.2026)

Tickets mit quelle:"wiederholung" entstehen an zwei Stellen: Spiel.tag.bereitschaftStarten() (Tages-Bereitschaft) und Spiel.wiederholungAnlegen() (nach „Lösung vorfĂŒhren"). Verbindlich:

8 · Plattform

Plattform = { name:"tauri"|"browser", version,
  impl,                                        // beim ersten Zugriff gewÀhlt: Tauri nur bei window.__TAURI__,
                                               // sonst Browser (src/plattform/plattform.js:6-9)
  speichern(daten) → Promise,                  // Tauri: invoke("speichern", {json}), atomar + Sicherungen
  fenster:{ modus(m), immerOben(b), position(ecke), groesse(b, hoehe), zeigen(), verstecken(),
            beenden(), zustand() },
  abzeichen(n, text),
  autostart(b) → Promise, autostartStatus() → Promise,
  datei:{ exportieren(name, text) → Promise, importieren() → Promise<String|null> },
  kann(funktion) → { ja, grund },              // FĂ€higkeitsmatrix, siehe unten
  an(ereignis, fn),
  info() → { os, sitzung, kann?, gruende?, selbsttest? } }

FĂ€higkeitsmatrix (KRN-02, verbindlich). Die acht FĂ€higkeiten sind immerOben, tray, autostart, hotkey, leiste, klickdurch, vollbildErkennen, benachrichtigen. kann(f) liefert immer {ja:boolean, grund:string|null}:

Ereignisse (Tauri): modus-extern, zweiter-start, ruhe, labor-beenden sowie tray-klick, hotkey, oeffnen — die HĂŒlle emittiert die drei letzteren selbst mit der Quelle als Ereignisnamen (emit(quelle) in fenster::umschalten, shell/src-tauri/src/fenster.rs:327); src/plattform/plattform-tauri.js reicht sie an die Seite. Die Abos in src/ui/start.js:102-105 sind damit keine toten Abos mehr (KRN-02, vorher Befund KRN-B06).

Rust-Befehle (invoke): speichern{json}, fenster_modus{modus}, fenster_groesse{b,h}, fenster_ecke{ecke}, fenster_zustand, fenster_knopf, immer_oben{an}, abzeichen{n,text}, autostart{an}, autostart_status, exportieren{name,inhalt}, importieren, plattform_info, ruhe{an}, beenden, selbsttest_ergebnis{json} (shell/src-tauri/src/main.rs:154-155).

Datei-API (KRN-04, verbindlich). importieren() löst in beiden Adaptern immer auf (String|null); Abbruch = null (Browser: cancel des Dateifelds; Tauri: Dialog-Abbruch). exportieren() ist nur true, wenn der Weg erfolgreich war (Tauri: Ok(true); Browser: Blob/URL/Klick ohne Fehler — ein echter Download ist dort nicht belegbar); Fehler/Abbruch = false, Tauri meldet zusĂ€tzlich datei-fehler {art, fehler} auf dem Bus. Die frĂŒheren toten Exporte Synchron-Laden und Systembenachrichtigung sind mit KRN-02 entfernt (0 Aufrufer, 0 Exporte).

9 · OberflÀche (UI)

9.1 Labor seit Ausbau 1.2 (Phase A „AufrĂ€umen“)

9.2 Welle 1, Sitzung S1 „SofortgefĂŒhl“ (Design – Spielspaß 2.0)

Spiel.regeln(inst) → { niveau, warnungen:bool, liveHaken:bool, liveGrund:bool, versuchAbzug:0|0.5|1 }
  // Stufenregeln (Design § 6): E: Warnungen an, Haken mit Grund, Versuche frei
  //   AP1: Warnungen erst ab Hilfestufe 2, Haken ohne Grund, −œ ★ ab dem 2. Abnahmeversuch (einmalig)
  //   AP2: keine Warnungen, Haken erst bei der Abnahme, −1 ★ ab dem 2. Versuch (einmalig). PrĂŒfung: wie AP2, ohne Abzug.
inst.abnahmen                     // Zahl der Abnahmeversuche (gab es schon); Spiel.abnahme zÀhlt hoch
Spiel.szene(inst, abnahme) → [ { ziel, art:"druckt"|"seite"|"adresse"|"gesperrt"|"haken"|"sicherung",
                                  pfad:[geraeteId], von, ende:geraeteId, text } ]   // headless, aus der Trace der Abnahme
Spiel.geaenderteGeraete(inst) → [geraeteId]   // Diff gegen das Startnetz: Konfig, Strom, startup, Kabel, neu
Spiel.tonPegel(ton, {modus, fokus}) → 0..1    // "aus"→0; Leiste oder ohne Fokus → 0; "leise" 0.18, "normal" 0.4
store "einst".ton = "aus"|"leise"|"normal"    // Standard "leise" (Spiel.EINST_STANDARD)
Spiel.postfachZiel() → 2 | 3                  // bis zum 2. erledigten Auftrag zwei Angebote verschiedener Kunden (erste Wahl), dann 3

9.3 Welle 1, Sitzung S2 „Bogen“ (Hub, TagesrĂ€tsel, Fehlerdex, Spieltagebuch)

Spielstand v:2 (Migration: fehlende Felder bekommen ihren Standardwert, ein v:1-Stand lĂ€dt unverĂ€ndert weiter). Aktueller Stand ist v:3 — siehe § 13 (Feld training, 07.10.2026).

st.dex          = { [injektor]: { gesehen:"2026-10-04", verstanden:null|"2026-10-05" } }
                  // gesehen = Auftrag mit dieser Fehlerart bestanden; verstanden = bestanden ohne bezahlte Hilfe (Hilfestufe < 4).
                  // Erst beim Abschluss erfasst – ein offener Auftrag verrĂ€t seine Ursache nie ĂŒber den Dex (R1).
st.tagesraetsel = { serie:0, [tag]: { nr, niveau, sterne, sek, hilfe, versuche, ziele:["g"|"y"|"w"], zeile } }
st.tagebuch     = [ { t, art:"sitzung", bis } | { t, art:"auftrag-ende", id, form, quelle, niveau, sek, sterne, hilfe, versuche, weiter:null|bool } ]  // höchstens 500
st.tag.aufwaermen = 0..n                                   // Mini-Karten im AufwÀrmen heute
inst.zielErst   = [ versuchNr|null ]                       // je Ziel: in welchem Abnahmeversuch zuerst erfĂŒllt (Spiel.abnahme)
inst.quelle     = 
 | "raetsel"                            // TagesrÀtsel: nicht im Postfach, verfÀllt am Folgetag

Spiel.dex.liste() → [ {id, titel, gruppe, zustand:"unbekannt"|"gesehen"|"verstanden", ab:stufe, symptom, erkennen:[text], erklaerung, quelle, gesehen, verstanden} ]
Spiel.dex.gruppen() → [ {id, titel, ehrentitel, ids:[injektor], verstanden, gesamt, fertig} ]
Spiel.dex.erfassen(inst, def) → {neu:[{id, titel, zustand}], titel:[ehrentitel]}      // aus Spiel.abschliessen
Spiel.tagebuch.sitzung() · .auftragEnde(inst, def, abnahme) · .weiter() · .aktiv() · .auswertung() → Text (zum Kopieren)
Spiel.raetsel.nummer(tag) · .def(tag, niveau) · .heute() · .starten() → inst · .teilen(tag) → Text
Spiel.hub.stand() → { tag, serie:{tage, urlaub, frei, zurueck}, naechster, aufwaermen:{ziel, erledigt}, raetsel, post, tagesziel, feierabend:null|{bilanz, ausblick} }
Spiel.hub.serie(tageSet, heute) → { tage, urlaub, frei }   // Serie mit Urlaubstagen: 2 Tage je Kalenderwoche fehlen erlaubt

9.4 Welle 2, Phase B „Netzplan, Dock, Akte, Verdacht“

Spiel.plan.sollNetz(inst)      → Netz   // Startnetz + Referenzlösung = so soll es sein (Störung: gesund, Projekt: Ziel). Nie mit Fehler.
Spiel.plan.aus(netz, {art, verdeckt}) → Plan
Plan = { art:"netzplan"|"skizze"|"tabelle", breite, hoehe,
         knoten:[ {id, name, typ, skin, x, y, ebene, adressen:[{port, ip, maske, vlan}], zeigen:[text]} ],
         linien:[ {a:{id, port}, b:{id, port}, trunk:bool, vlan:null|n} ],
         netze:[ {cidr, vlan, geraete:[id]} ],
         tabelle:[ {id, name, port, ip, maske, gw, dns, vlan, dhcp:bool} ],   // verdeckte Felder = "?"
         verdeckt:[ {id, feld} ] }
Spiel.plan.art(inst) → "netzplan" (E) | "skizze" (AP1, Tabelle daneben) | "tabelle" (AP2, Gateway/DNS der Rechner verdeckt) | ticket.plan.art
Spiel.plan.fuer(inst) → Plan (gecacht je Instanz)
Spiel.plan.diff(a, b) → [ {geraet, pfad, soll, ist} | {geraet, kabel:"fehlt"|"zuviel", gegen} ]   // Konfig (running), Strom, VLAN-Datenbank, Kabel
Spiel.plan.abweichungen(inst) → [geraeteId]          // Plan ↔ Labor, fĂŒr „Abweichungen markieren“ (kostet wie Hilfestufe 4)

inst.akte     = [ {n, t, art:"ping"|"trace"|"kabel"|"befehl"|"plan", von, nach, befund, schicht:null|1..7, grund:null|code, ok, wichtig:false} ]
Spiel.akte.ausPing(inst, {von, nach, ergebnis}) · .ausKabeltest(inst, {kabel, oben, grund}) · .stern(inst, n) · .liste(inst)
  // befund = was ein echtes Werkzeug zeigt („ZeitĂŒberschreitung“, „Zielhost nicht erreichbar“); Grund/Schicht nur im Einstieg sichtbar
inst.verdacht = { schicht, ursache:injektor, geraet, t, vorEingriff:bool, treffer:null|"schicht"|"ursache"|"voll" }
Spiel.verdacht.optionen(inst) → { schichten:[{n, name, text}], ursachen:[{id, titel, schicht}] (4 Fehlerarten, eine richtig), geraete:[{id, name}] } | null
Spiel.verdacht.setzen(inst, {schicht, ursache, geraet}) · .bewerten(inst) → {treffer, gesetzt, vorEingriff, richtig:{ursache, schicht, geraete}, text}
  // Ursachen sind Fehlerarten (= Fehlerdex-EintrĂ€ge), nicht SimulationsgrĂŒnde: „Falsches Standardgateway“ statt „Gateway antwortet nicht“
  // E freiwillig (+10 % Lohn bei vollem Treffer vor dem Eingriff), AP1 ohne Verdacht −œ ★, AP2 −1 ★ (Spiel.regeln(inst).verdacht)
st.werkzeuge  = { kabeltester:bool, netzpruefer:bool }   // Shop „Werkzeuge“; inst.netzpruefer = true schaltet die „!“ im AP-Niveau zu

9.5 Welle 2, Phase C „Terminal“

Modell.osVon(geraet) → "windows" | "linux" | "ios" | null     // pc → windows, server/nas → linux (geraet.os ĂŒbersteuert), switch/router/firewall → ios
CLI.sitzung(netz, id, {verlauf, einstieg, tipps:"alle"|"fehler"|"keine"})   // Tipp-Zeilen: Einstieg alle, AP1 nur nach Fehlern, AP2 keine
CLI.eingabe(s, zeile) → { 
, befund?:"eine Zeile fĂŒr die Akte", ok?:bool }   // Diagnosebefehle (ipconfig, ping, nslookup, ip a, dig, systemctl status 
)
CLI.host.*                                // gemeinsame Helfer (cli/host-terminal.js); Befehle: cli/host-windows.js, cli/host-linux.js
CLI.traceMerken(netz, trace)              // letzte Aufzeichnung je Netz – tcpdump zeigt sie
inst.befehle = [ {t, geraet, befehl, ok} ]                      // ausgefĂŒhrte Terminalbefehle (fĂŒr Zielart „befehl“, Abzeichen „Von unten nach oben“)
inst.antworten = { [ziel.id]: "eingetragener Wert" }             // Zielart „antwort“ (Eingabe in der Mappe)

Arbeitsziele (C4, spiel/arbeitsziele.js): Netzziele prĂŒft die Simulation am Netz; Arbeitsziele prĂŒfen, was der Spieler im Auftrag getan hat. Sim.pruefeZiel kennt sie nicht – nur Spiel.zielPruefen(netz, ziel, inst).

Ziel {typ:"befehl", geraet, muster:"^ipconfig( /all)?$", beispiel:"ipconfig", text}
     // erfĂŒllt, wenn ein passender Befehl (Regex, ohne Groß/klein) im Auftrag auf dem GerĂ€t fehlerfrei lief
Ziel {typ:"antwort", id:"ip", frage:"Welche IP-Adresse hat die Kasse?", pruefen:{art:"ip"|"maske"|"gw"|"dns"|"mac", geraet, port?:"eth0"}, text}
     // Soll kommt immer aus dem aktuellen Netz (Sim.adresse bzw. hw.macs) – nie aus dem Ticket; MAC in jeder Schreibweise
Spiel.istArbeitsziel(ziel) · Spiel.arbeitsziel(inst, ziel, netz?) → {ok, grund:null|"BEFEHL_FEHLT"|"ANTWORT_FEHLT"|"ANTWORT_FALSCH", text}
Spiel.antwortSoll(netz, ziel) → "192.168.1.10" | null · Spiel.antwortGleich(art, a, b) · Spiel.antwortSetzen(inst, ziel, wert) → PrĂŒfergebnis (Bus „antwort“)
Spiel.arbeitszieleErfuellen(inst)            // Durchspiel-Test: Befehle aus ziel.beispiel im Terminal ausfĂŒhren, Antworten aus dem Netz eintragen
Lösungsschritt {geraet, terminal:"netsh interface ip set address 
", text}   // lĂ€uft in einer Terminal-Sitzung (Windows/Linux) auf dem GerĂ€t
ticket.art = 
 | "terminal"                  // Terminal-Auftrag: Arbeitsziele + ggf. ein Netzziel; ohne Fehler im Netz erlaubt (nur Arbeitsziele)

9.6 Welle 3, E1 „Formen, Wahl, Ereignisse, Provisorium“

Auftragsformen (spiel/formen.js): Spiel.FORMEN[form] = {titel, sym, text} fĂŒr stoerung, projekt, terminal, forensik (Fernwartung: Netz unsichtbar, nur das Terminal eines Rechners), audit (Kundenplan prĂŒfen), beratung (Adressplan rechnen), hotline (RĂŒckfragen am Telefon, dann Störung). Spiel.formVon(def) → def.form || def.art (wartung → stoerung). Generierte Formen:

inst.gen = {skill, seed, opts} | {form:"forensik"|"audit"|"beratung", seed, opts:{stufe, kunde}}
Spiel.generiereForm(form, seed, opts) → def      // deterministisch, id "form-<form>-<kunde>-<stufe|auto>-<seed>", registriert in Spiel.generierte
Spiel.risikoVon(def) → {stufe:1..3, text:"gering"|"mittel"|"hoch"}   // aus dem Niveau: AbzĂŒge je Versuch (E 0, AP1 œ, AP2 1 ★)

Neue Arbeitsziele (wie befehl/antwort, geprĂŒft mit inst):

Ziel {typ:"tabelle", id, basis:"192.168.20.0/24", zeilen:[{name, hosts}], spalten:["netz","praefix","erster","letzter","broadcast"], text}
inst.tabelle  = { ["zeile.spalte"]: "Eingabe" }     // Soll aus den IP-Helfern (Spiel.beratung.soll(ziel)), Vergleich ohne Leerzeichen; PrĂ€fix „/26“ oder „26“
Ziel {typ:"audit", fehler:["id.feld"], text}        // die 1–3 Abweichungen im Kundenplan (def.kundenplan)
inst.audit    = ["id.feld", 
]                       // markierte Zellen; erfĂŒllt = alle gefunden, nichts Falsches markiert
def.kundenplan = Plan (wie Spiel.plan.aus) mit 1–3 geĂ€nderten Tabellenwerten
Ziel {typ:"notiz", min:20, text}                     // Änderungsnotiz fĂŒr den Kunden (Variante „sauber“)
inst.notiz    = "
"
def.hotline   = { anruf:"
", max:3, fragen:[{id, text, antwort, wert:"gut"|"neutral"|"schlecht", warum}] }
inst.hotline  = { gefragt:[id], fertig:bool }       // Bewertung: gute Fragen → Ruf; Antworten als Karten in der Akte (art "hotline")

Postfach als Wahl (spiel/mischer.js): nach der Einstiegs-Wahl 3 Angebote (ab Stufe 2: 4), möglichst verschiedene Formen und Kunden. Je Form ist höchstens ein Angebot neu im Postfach; die Story-Reihenfolge der Handtickets bleibt je Form erhalten.

Spiel.mischer.waehlen({kandidaten:[{form, kunde, gewicht, schluessel}], offen:[{form, kunde}], verlauf:[form], n, z}) → [kandidat]   // rein, ohne Ticketbau
Spiel.mischer.gesperrt(verlauf) → form|null          // die Form der letzten zwei AbschlĂŒsse, wenn gleich
Spiel.postfach()                                     // gesperrte Form steht unten – der Hub schlĂ€gt sie nie als nĂ€chstes vor
st.erledigt[i].form                                  // Form jedes Abschlusses (Mischer, Tagebuch)

Provisorium oder sauber? (spiel/varianten.js): Handtickets mit Änderungen an IOS-GerĂ€ten (Liste Spiel.VARIANTEN_TICKETS) bieten beim ersten Öffnen eine Wahl. Provisorium: nur die Netzziele, kein Neustart-Test, Lohn × 0,6, Schuld → nach 3 weiteren AbschlĂŒssen kommt der Folgeauftrag („Das Provisorium von neulich 
“: dasselbe Netz, der Fehler ist nach einem Neustart zurĂŒck, Ziele + Sicherung). Sauber: Netzziele + „gespeichert“ je geĂ€ndertem IOS-GerĂ€t + Änderungsnotiz (notiz), Lohn × 1,2.

inst.variante = null|"provisorium"|"sauber"          // ohne Wahl gelten die Grundziele
Spiel.varianten.fuer(def) → [{id, titel, text, lohnFaktor}] | null
Spiel.varianten.ziele(inst) → Ziele der gewĂ€hlten Variante · Spiel.varianten.waehlen(inst, id)
st.schulden = [ {id, ticket, kunde, seit:erledigtAnzahl, faellig:erledigtAnzahl+3, folge:iid|null, erledigt:bool} ]
Folgeauftrag-id "<ticket>-folge" (Spiel.varianten.folgeDef(def))

Ereignisse (spiel/ereignisse.js, Einstellung st.einst.ereignisse = "aus"|"selten"|"normal", Standard „selten“): höchstens eines je 15 (normal) bzw. 30 (selten) Minuten aktiver Zeit; deterministisch aus Spielstand-Seed und ZĂ€hler; jedes mit ErklĂ€rsatz; nie Fortschrittsverlust (Stufe, AuftrĂ€ge, Kunden bleiben; NetzĂ€nderungen laufen ĂŒber den Verlauf).

Spiel.EREIGNISSE[id] = {titel, sym, bedingung(ctx) → bool, ausloesen(ctx) → {text, warum, aktion?}}
   // stromausfall (offener Auftrag mit IOS-GerĂ€t: alle IOS-GerĂ€te neu starten – nur Gespeichertes bleibt), kabelschaden,
   // provider (Internet beim Kunden aus, bis „Provider anrufen“), praktikant (zweiter kleiner Fehler), weiterempfehlung (neuer Kunde
   // bietet einen Auftrag an), notfall (Notfall-Auftrag mit Frist – du wĂ€hlst die Reihenfolge)
st.ereignisse = { aktivMs, letzteMs, n, liste:[{t, id, kunde, text}] }
Spiel.ereignisse.tick(ms) → Ereignis|null · .ausloesen(id, ctx) (Tests) · .aktivZaehlen(ms)

9.7 Welle 3, E2 „Kundenakte und Kompetenzkarte“

Kundenakte (spiel/kundenakte.js, OberflĂ€che in der Ansicht „Kunden“, PrĂ€fix ka-):

st.kundenakte = { [kunde]: { punkte:0, atlas:[geraeteId], kapitel:0..3, gelesen:[nr], antworten:{[nr]: index} } }
Spiel.kundenakte.daten(kunde) · .vertrauen(kunde) → 1..5          // aus Punkten: ★ ≄ 4,5 +1 · „sauber“ +1 · Notfall rechtzeitig +1 · Provisorium 0
Spiel.kundenakte.atlasNetz(kunde) → Netz                           // das dokumentierte Netz des Kunden (Vorlage mit festem Seed) fĂŒr den Plan-Erzeuger
Spiel.kundenakte.atlas(kunde) → {plan, hell:[id], gesamt, komplett}  // GerĂ€te leuchten, an denen man gearbeitet hat (geĂ€ndert oder geprĂŒft)
Spiel.kundenakte.nachAbschluss(inst, def, abnahme, {geaendert, variante}) → {neuHell:[id], vertrauen:{vorher, nachher}, kapitel:nr|null}
Spiel.kundenakte.kapitel(kunde) → [{nr, titel, frei:bool, ab:vertrauen, gelesen}] · .lesen(kunde, nr) · .antworten(kunde, nr, i) → {richtig, erklaerung}
Spiel.kundenakte.baustellen(kunde) → offene Schulden (Provisorium, Folgeauftrag noch nicht erledigt)
DATEN.geschichten[kunde] = [ {nr:1..3, ab:2..4, titel, text, senior, frage:{text, optionen, richtig, erklaerung}, quelle} ]

Kapitel 1 öffnet bei Vertrauen 2, Kapitel 2 bei 3, Kapitel 3 bei 4. Jedes Kapitel: kurze Szene beim Kunden mit kleiner Wendung, ein Satz des Seniors (Haltung: Gegenfrage statt Lösung, trocken-warm), eine Frage mit ErklĂ€rung, Quelle fĂŒr die fachliche Aussage.

Kompetenzkarte (spiel/kompetenz.js, im Lernstand statt der Fertigkeiten-Liste, PrÀfix kk-):

Spiel.KOMPETENZ_REGIONEN = [ {id, titel, skills:[id
]} ]          // 6 Regionen, zusammen genau die 27 Fertigkeiten aus DATEN.skills
Spiel.kompetenz.karte() → { regionen:[{id, titel, felder:[{id, name, zustand, box, nachbarn:[id]}]}], sichtbar, gesamt:27 }
   // zustand: "nebel" (weder geĂŒbt noch neben einem geĂŒbten Feld noch freigeschaltet) | "offen" | Lernmotor-Stufe („angefangen“ 
 „gemeistert ★“)

9.8 Auftrag P „Politur und Bindung“ (Design – Spielspaß 2.0, § 20/§ 21)

Erste Stunde (spiel/erstestunde.js, § 20 F3): Ein neuer Spielstand bekommt statt Zufall eine gestaltete Folge der ersten sechs AuftrĂ€ge; danach fĂŒllt der Mischer.

Spiel.ERSTE_STUNDE = [ {id:"kabel", ticket:"salon-01"}, {id:"stoerung", ticket:"salon-02"}, {id:"hotline", ticket:"salon-hotline", klingelt:true},
                       {id:"fernwartung", form:"forensik", opts:{kunde:"salon"}}, {id:"variante", ticket:"salon-05"},
                       {id:"empfehlung", form:"audit", ereignis:"weiterempfehlung", opts:{kunde:"baeckerei"}} ]
st.ersteStunde = { fertig:bool, ereignis:bool }      // nur ein NEUER Stand beginnt mit fertig:false (Spiel.ergaenzer.ersteStunde);
                                                     // fehlt das Feld (alter Stand, Tests mit leererStand): vorbei
inst.kuratiert = 1..6 · inst.kuratiertId = "hotline" 
 · inst.klingelt = true   // Postfach-Reihenfolge, Anruf
Spiel.ersteStunde.aktiv() · .auffuellen() → [inst] · .ruhig() · .abholen() → Karte|null · .stand() → {aktiv, geschafft, gesamt, naechster}
ergebnis.ereignis = {id:"anruf"|"weiterempfehlung", sym, titel, text, warum?, aktion:"abheben"|"neu", iid}   // Spiel.abschliessen
Spiel.ereignisse.ausloesen(id, {inst?, form?, kunde?, stufe?})   // ohne inst gilt der offene Auftrag; form/kunde/stufe sind WĂŒnsche

Probe je Form (spiel/szene.js + ui/szene.js, § 20 F4): eigene, höchstens rund 3 s lange Funktionsprobe fĂŒr die neuen Formen, ĂŒberspringbar wie die Paket-Szene.

Spiel.szeneForm(inst) → null (Störung, Projekt, Terminal: Paket-Szene genĂŒgt)
  | {art:"hotline", person, symbol, farbe, satz, minuten}          // Hörer wird aufgelegt, Dank, GesprÀchsdauer (statt Sprechblase)
  | {art:"fernwartung", geraet, name}                              // Sitzungsfenster: ✓ auf dem Bildschirm, „Verbindung getrennt“, „online ✓“ – dann das Netz
  | {art:"audit", kunde, korrigiert, tag}                          // Stempel „GEPRÜFT ✓“ mit Datum auf der Skizze des Netzes
  | {art:"adressplan", basis, bereiche:[{name, netz, praefix, links, breite}]}   // Bereiche leuchten nacheinander im Adressraum (Prozent)
UI.szene.abspielen(zeilen, {
, form, aufdecken})   // Hotline/Fernwartung: höchstens 1 Paketweg; Audit/Adressplan: keine Paketfahrt
UI.labor.fernFertig()                               // Sitzungsfenster in den Zustand „getrennt“; aufdecken() hebt die Fernwartung auf (auch beim Überspringen)

Vertrauen zahlt aus (spiel/kundenakte.js, § 20 F5 – R4 „jede Belohnung öffnet etwas“):

Spiel.VERTRAUEN_LOHN = {empfehlung:3, rabatt:4, rabattAnteil:0.2}
st.empfehlungen = { [kunde]: {von, tag} }           // durch Empfehlung eine Stufe frĂŒher offen
st.kundenakte[kunde].empfohlen = true                // dieser Kunde hat seine (einmalige) Empfehlung ausgesprochen

10 · DHCP-Tiefe (D1 aus Plan – Ausbau 1.2.md) — verbindlicher Vertrag (05.10.2026, Stand 2.9.0)

Wiederhergestellt am 31.10.2026 fĂŒr 3.0.0 (Commit 9716b08 hatte § 10–§ 18 gelöscht). Datenformen zuerst hier, dann im Code; Zahlen ausschließlich aus docs/STAND.md.

Regressionsschutz. Nach jeder Änderung an src/sim/ oder src/modell/: node tools/sim-stand.js — „Simulation unverĂ€ndert" oder die Abweichung ist ausdrĂŒcklich als gewollt benannt (mit BegrĂŒndung). Die Testzahl misst docs/STAND.md, nicht dieser Abschnitt.

10.1 Konfiguration (geraet.running, wandert in den Spielstand)

/* Router/Firewall als DHCP-Server: running.dhcp */
dhcp = {
  an: false,                       // Server an/aus
  pools: [{
    name, netz, maske, gw, dns,    // vorhanden
    start: "", anzahl: 50,         // Adressbereich wie host.js
    domain: "",                    // Option 15
    leaseS: 86400,                 // Lease-Dauer in Sekunden; 0/fehlend = Sim.T.LEASE
    reservierungen: [{ mac:"02:00:
", ip:"192.168.10.20", name:"drucker" }],
  }],
  ausgeschlossen: [{von, bis}],
}
/* Host (pc/server/nas) als DHCP-Server: running.dienste.dhcp – dieselben Pool-Felder
   (start, anzahl, domain, leaseS, reservierungen); ausgeschlossen gilt auch hier. */
/* Switch: running.snooping */
snooping = { an:false, vertraut:["Gi0/1"] }   // vertraute Ports (Richtung Server)

Einheitlichkeit ist Pflicht: Router-Pool und Host-Pool haben dieselbe Form; fehlende Felder werden defensiv gelesen (Standard), damit alte SpielstÀnde laden.

10.2 Laufzeit (netz.zustand[id])

leases:   { [ip]: { mac, bis, t1, t2, hostname, abgelaufen:false } }
dhcp:     { [port]: { ip, maske, gw, dns, server, bis, t1, t2, domain } }  // Client-Sicht
snooping: { [port]: { verworfen:0 } }

t1 = bis - leaseDauer/2 (Erneuerung bei 50 %), t2 = bis - leaseDauer/8 (Rebind, nur Anzeige). Alte Leases ohne t1/t2 gelten als „kein T1 bekannt", nicht als Fehler.

10.3 Verhalten (Simulation)

  1. Ablauf in virtueller Zeit. Ist L.t > l.bis, gilt die Lease als abgelaufen: Sim.adresse liefert quelle:"keine", der nĂ€chste Zugriff holt neu. Kein neuer Timer, kein neues Ereignis — Ablauf wird beim Zugriff festgestellt.
  2. Erneuerung bei 50 %. GĂŒltige Lease und L.t >= t1: Renew (Unicast Request/Ack); sonst Discover.
  3. Reservierung. Passt die Client-MAC, wird diese IP angeboten (auch außerhalb start..anzahl), aber nur wenn frei; sonst kein Angebot, Grund DHCP_RESERVED_BUSY.
  4. Optionen. Der Ack trÀgt domain (Option 15); leaseS kommt aus dem Pool.
  5. Adresskonflikt. Vor dem Angebot prĂŒft der Server gegen GerĂ€te und fremde Leases; doppelt belegt → nĂ€chste Adresse, Konflikt wird vermerkt (leases[ip].konflikt mit Zeitpunkt). show ip dhcp conflict gibt echte Zeilen aus.
  6. Rogue-DHCP. Ein zweiter Server im LAN ist erlaubt; der Client nimmt das erste Angebot und kann falsches Gateway/DNS erhalten. Grund DHCP_ROGUE_OFFER (Schicht 7).
  7. DHCP-Snooping. Bei snooping.an werden Server-Nachrichten (UDP 67 → 68) von nicht vertrauten Ports verworfen; ZĂ€hler steigt, Client bekommt kein Angebot. Grund DHCP_SNOOPING_BLOCKED (Schicht 2). Client-Nachrichten (68 → 67) sind immer erlaubt.

10.4 Neue GrĂŒnde (Codes) — genau diese fĂŒnf, keine weiteren

CodeSchichtAusgelöst wenn
DHCP_LEASE_EXPIRED7Lease abgelaufen und in diesem Lauf kein neues Angebot
DHCP_RESERVED_BUSY7Reservierung zeigt auf eine nicht freie Adresse — der Server gibt kein Angebot
DHCP_ROGUE_OFFER7Client nimmt ein Angebot eines unerwarteten Servers
DHCP_SNOOPING_BLOCKED2Server-Antwort an einem nicht vertrauten Switch-Port
DHCP_CONFLICT7Eine Adresse liegt auf mehr als einem GerĂ€t; der Server ĂŒberspringt sie und nennt diesen Grund

Vorrang, wenn mehrere zutreffen: DHCP_RESERVED_BUSY → DHCP_CONFLICT → DHCP_POOL_EMPTY → DHCP_LEASE_EXPIRED → DHCP_NO_OFFER. Der Client wiederholt den Discover bis zu 3×; ein Server-Grund steht einmal je Versuch in der Trace, das Ergebnis grund des Aufrufs ist genau einer. Jeder Code braucht Eintrag in Sim.GRUENDE, Lehrtext in drei Tiefen mit Quelle, Injektor, mindestens ein Ticket je Stufe und Aufnahme in die Grundcode-Liste der Tests.

10.5 Trace — unverĂ€ndert im Format

Keine neue art, kein neues Pflichtfeld im Ereignisobjekt (das optionale Warnfeld warnung aus § 5.2 bleibt die einzige ErgĂ€nzung). Renew nutzt die vorhandenen Typen request/ack; neue app.felder sind erlaubt (PDU-Ansicht). tests/sim-gruende.test.js prĂŒft die SchlĂŒsselmenge eines Ereignisses exakt.

10.6 OberflÀche und Konsole

12 · Klassenraum (Lehrer/SchĂŒler, Hobby-Ebene) — Vertrag

Stand 3.0.0 (31.10.2026/01.11.2026). Dieses Kapitel ersetzt den am 30.10.2026 mit Commit 9716b08 gelöschten Abschnitt § 12 und fĂŒhrt ihn auf den gemessenen Code-Stand fort. Quellen: docs/entwicklung/Klassenraum/ A–D, historischer Stand 32cf209, src/spiel/klassenraum.js, src/spiel/klassenraum-codec.js, src/ui/klassenraum.js, src/spiel/abnahme.js, tests/klassenraum*.test.js, tests/ui-klassenraum*.test.js.

Kern in drei SĂ€tzen. Die Lehrkraft sagt einen kurzen Auftragscode an; jedes GerĂ€t baut daraus denselben Auftrag, weil AuftrĂ€ge deterministisch aus einem Seed entstehen. Es gibt keine Bewertung: der Ergebnis-Code trĂ€gt Sterne, Fehlversuche und Dauer fĂŒr den Lernstand des Einzelnen, keine Note, keine Rangfolge, keinen Vergleich. Ohne Konto, ohne Server, ohne Netz — Stufe A (Codec und Determinismus) und B (OberflĂ€che, Ampel, Export/Import) tragen den Unterricht vollstĂ€ndig; C (lokaler Server) und D (QR) sind nicht gebaut und bleiben 3.1 (§ 12.6/§ 12.7).

GegenstandFestlegung
AuftragscodeNL-XXXX-XX — 10 Zeichen, 30 Bit Nutzteil + 2 PrĂŒfzeichen (mod 31 / mod 32), Alphabet ohne I, O, 0, 1
Ergebnis-CodeE-XXXX-XXX — 9 Zeichen, 25 Bit (Sitzung 5 · Platz 5 · halbe Sterne 4 · Fehlversuche 2 · Dauer 9) + 2 PrĂŒfzeichen
HilfecodeH-XXXX-XX — 9 Zeichen, 20 Bit (Sitzung 5 · Platz 5 · erfĂŒllte Ziele 5 · offene Ziele 5) + 2 PrĂŒfzeichen (§ 12.5)
Speichereigener SchlĂŒssel klassenraum neben labor, einst, sandbox, labor-sicherung; fassung: 2 seit 3.0.0 (§ 12.1)
ÖffnungswegSpiel.klassenraum.ausCode(code) → Spiel.instanzErstellen({
, quelle:"klassenraum", ohneFlow:true}) → inst.klassenraum = {sitzung, platz, code} → Spiel.oeffnen(iid)
AbnahmeSpiel.klassenraum.abnehmen(inst) (delegiert an Spiel.abnahme), Ergebnis-Code nur bei bestanden; die Instanz wird danach entfernt, Geld/Ruf/Karriere bleiben unberĂŒhrt (src/spiel/abnahme.js)
Bewertungkeine — ausdrĂŒcklich Nutzerentscheidung 09.10.2026; wer spĂ€ter bewerten will, Ă€ndert einen Vertrag, nicht eine Einstellung

12.1 Datenform — store "klassenraum", fassung: 2

Die Sitzung liegt nicht im Spielstand (store "labor"), sondern unter einem eigenen SchlĂŒssel: ein Schulrechner braucht sie unabhĂ€ngig vom Spieler-Fortschritt. Geschrieben wird entprellt (1500 ms, src/kern/basis.js); erzeugen und ergebnisEintragen rufen zusĂ€tzlich store.sofort().

/* store "klassenraum" — Wurzel. Ab 3.0.0: mehrere Sitzungen, eine aktive (KLR-01a). */
Speicher = {
  fassung: 2,                     // Schemaversion dieses Teils; 1 = Altstand (eine sitzung)
  programm: "<LABOR_VERSION>",
  platz: null,                    // eigener Platz dieses GerÀts, 0..31, null = keiner
  zuletzt: 1759706400000,         // ms, jetzt()
  sitzungen: [Sitzung, 
],        // alle Sitzungen dieses GerÀts, Reihenfolge = Anlage
  aktiv: 7,                       // id der aktiven Sitzung | null
  letzte: null,                   // zuletzt gelesener Code/Ergebnis (Anzeige)
  zaehler: 3                      // monoton, speist Sitzungskennung und Wunsch-Variante
}
/* Jede Sitzung hat genau diese 16 Felder (3.0.0: diagnose neu): */
Sitzung = {
  id: 7,                          // Sitzungskennung 1..31 (5 Bit im Auftragscode)
  titel: "Klassenraum-Auftrag",
  art: "hand",                    // "hand" | "generiert"
  ticketId: "salon-01",           // genau eines von ticketId | (skill + seed)
  skill: null,                    // Fertigkeit aus DATEN.skills, wenn generiert
  index: 0,                       // Index in TABELLE_AUFTRAEGE bzw. TABELLE_SKILLS
  variante: 42,                   // 8 Bit; seed = variante + 1
  seed: 43,                       // 1..256
  eigene: true,                   // Kanonisierung: eigene Fassung ja/nein
  schritte: 5,                    // Lösungsschritte aus der Kanonisierung (additives Feld)
  code: "NL-BAAA-BB",             // gedruckter Auftragscode
  dauerMin: 10,                   // geschÀtzte Dauer in Minuten
  plaetze: 20,                    // KlassengrĂ¶ĂŸe, 0 = nicht eingestellt, sonst 1..31
  erstellt: 1759706400000,        // ms, jetzt()
  ergebnisse: {},                 // Abbildung platz → {platz, sterne, dauerS, versuche, code, zeit, quelle}
  diagnose: {}                    // 16. Feld (3.0.0, KLR-05a): {[schritt]: anzahl} — anonyme H-Code-ZĂ€hler je Schritt; fehlt = {}
}

Migration fassung: 1 → 2 (3.0.0, verbindlich). Ein Altstand mit sitzung: Sitzung | null wird beim Lesen verlustfrei ĂŒbernommen: sitzungen = sitzung ? [sitzung] : [], aktiv = sitzung ? sitzung.id : null; alle ĂŒbrigen Felder bleiben. Ein fehlendes oder unbekanntes fassung wird wie 1 behandelt. Ein zu neuer Stand (fassung > 2) wird abgewiesen, nicht umgeschrieben.

Export/Import als Datei: {"format":"netzwerk-labor/klassenraum","fassung":2,"programm":
,"zeit":
, "sitzungen":[Sitzung,
],"aktiv":id}. Der Import nimmt auch fassung: 1 ({
,"sitzung":
}) an und migriert; unbekannte Felder bleiben erhalten (verlustfrei, Object.assign-Verhalten aus 2.9.0). Beide Richtungen werfen nie — fremdes/beschĂ€digtes JSON ergibt {fehler:"format", grund}.

12.2 API — Spiel.klassenraum

18 Namen (Stand 2.9.0, gemessen an der RĂŒckgabe von src/spiel/klassenraum.js:448-450; 3.0.0 kommt auf 22 öffentliche Namen): erzeugen · ausCode · sitzung · ergebnisCode · ergebnisLesen · ergebnisEintragen · hilfeCode · hilfeLesen · exportieren · importieren · netzkennwert · tauglicheFertigkeiten · platz · platzSetzen · plaetze · plaetzeSetzen · abnehmen · tabellen.

AufrufRĂŒckgabeZusicherung
erzeugen({ticketId?, skill?, seed?, dauerMin?, titel?, plaetze?, bestaetigen?})sitzung \{warnung, sitzung?}legt eine Sitzung an und schreibt sofort; fehlender Seed wird gezogen und steht im Code; hat die aktive Sitzung bereits Ergebnisse, wird sie ohne bestaetigen: true nicht ersetzt — RĂŒckgabe {warnung:"sitzung-hat-ergebnisse"} (3.0.0)
ausCode(code){ticketId, seed, art, sitzung, index, variante, skill, eigene, schritte, 
} \{fehler, grund} \nullwirft nie; null nur bei leerer Eingabe; normalisiert Groß-/Kleinschreibung und Trennstriche
sitzung()aktive Sitzung \nullliest nur store "klassenraum", nie den Spielstand
sitzungen()[{id, titel, art, code, erstellt, plaetze, ergebnisse, aktiv}]neu 3.0.0 (KLR-01a): Übersicht aller Sitzungen, Kopien, keine Innenreferenzen
sitzungWaehlen(id){ok, sitzung} \{fehler:"sitzung", grund}neu 3.0.0: setzt aktiv; unbekannte id ist ein Fehler, kein stilles Ersetzen
ergebnisCode(inst, abnahme)"E-
" \{fehler, grund}trĂ€gt Sitzung, Platz, halbe Sterne, Fehlversuche, Dauer und PrĂŒfsumme; nur bei bestandener Abnahme
ergebnisLesen(code){ok, sitzung, platz, sterne, dauerS, versuche, code} \{fehler, grund}wirft nie; prĂŒft PrĂŒfsumme und Codewert
ergebnisEintragen(code){ok, neu, platz, sterne} \{fehler, grund}idempotent (derselbe Code Àndert nichts, neu:false); fremde Sitzung ist ein Fehler
hilfeCode(inst) · hilfeLesen(code)"H-
" \{fehler, grund} · {ok, sitzung, platz, schritt, offen, code}siehe § 12.5; wirft nie
exportieren() · importieren(text)JSON-String · {ok, sitzungen, aktiv} \{fehler, grund}verlustfrei, ĂŒberstehen Neustart; 3.0.0: alle Sitzungen (§ 12.1)
netzkennwert(netz)6 Zeichen \""siehe § 12.4; keine Ausnahme
tauglicheFertigkeiten()[{skill, index, ersterSeed}]nur Fertigkeiten, fĂŒr die hier wirklich ein Auftrag baubar ist
platz() · platzSetzen(z) · plaetze() · plaetzeSetzen(z)0..31 · {ok} \{fehler:"wahl"}eigener Platz · KlassengrĂ¶ĂŸe der aktiven Sitzung (0 = nicht eingestellt)
abnehmen(inst){ok, klassenraum, bestanden, sterne, abnahme, lernen, def, inst, 
}Spiel erhĂ€lt kein Geld/keinen Ruf; lernen lĂ€uft ĂŒber den normalen Lernmotor
tabellen(){auftraege, skills, 
}PrĂŒfflĂ€che; eingefrorene Tabellen (§ 12.8)

3.0-Erweiterung KLR-04a (Sitzungen löschen/ersetzen, keine stille Kappung). Verbindlich:

AufrufRĂŒckgabeVerhalten
erzeugen({
, bestaetigen:true})sitzung \{warnung:"sitzung-hat-ergebnisse"}ersetzt die aktive Sitzung mit Ergebnissen nur mit bestaetigen:true; die alte Sitzung bleibt in sitzungen[] erhalten (KLR-01a). 32. Sitzung (alle Kennungen 1..31 belegt) → {fehler:"sitzung", grund} ohne Änderung.
sitzungLoeschen(id, {bestaetigen?}){ok:true, geloescht, aktiv} \{warnung:"sitzung-hat-ergebnisse"} \{fehler:"sitzung", grund}neu 3.0.0 (KLR-04a): löscht eine gespeicherte Sitzung samt Ergebnissen. Unbekannte id → Fehler, kein stilles Nichts. Hat die Sitzung Ergebnisse, nur mit bestaetigen:true (sonst Warnung ohne Änderung). War sie aktiv, wird aktiv auf die zuletzt angelegte verbleibende Sitzung gesetzt oder auf null; frei gewordene Kennung darf wieder vergeben werden; zaehler bleibt monoton.
erzeugen({
, plaetze})
kein stilles Klemmen mehr: plaetze > 31 oder negativ → {fehler:"wahl", grund} ohne Anlegen (vorher still 0); 0 bleibt „nicht eingestellt".

Abnahme: tests/klassenraum-sitzungen.test.js (oder neue Datei) — 31 Sitzungen anlegen → 32. ergibt {fehler:"sitzung"} und Speicher unverĂ€ndert; sitzungLoeschen ohne bestaetigen bei Ergebnissen → Warnung ohne Änderung; mit bestaetigen:true → Sitzung weg, aktiv sauber, freie Kennung wieder vergebbar; Gegenprobe rot bei eingeschmuggeltem Fehlverhalten.

Ergebnis-Korrektur (KLR-04a, 31.10.2026) — ergĂ€nzt die Tabelle:

AufrufRĂŒckgabeVerhalten
ergebnisEintragen(code, {ersetzen?, bestaetigen?}){ok, neu, platz, sterne} \{ok:true, neu:false, ersetzt:true} \{warnung:"platz-hat-ergebnis"}derselbe Code bleibt idempotent (neu:false, Grund „doppelt"); ein anderer Code am selben Platz ersetzt nur mit ersetzen:true und bestaetigen:true, sonst {warnung:"platz-hat-ergebnis"} ohne Änderung.
ergebnisEntfernen(platz, {bestaetigen?}){ok:true, entfernt:platz} \{fehler:"platz", grund} \{fehler:"wahl", grund}neu 3.0.0 (KLR-04a): entfernt den Eintrag am Platz aus der aktiven Sitzung; kein Eintrag dort → {fehler:"platz"} (kein stilles Nichts); Platz außerhalb 0..31 → {fehler:"wahl"}. Die RĂŒckfrage [Abbrechen][Ersetzen][Entfernen] fĂŒhrt die OberflĂ€che (task-41).

Grenz-Signale: ergebnisEntfernen meldet unzulĂ€ssige PlĂ€tze als wahl; ergebnisEintragen behĂ€lt fĂŒr kaputte Codes die Vertragsklassen aus § 12.3 (lĂ€nge/zeichen/prĂŒfziffer), damit bestehende Tests und UI-Texte gĂŒltig bleiben. erzeugen({plaetze > 31}) → {fehler:"wahl"} (siehe voriger Absatz).

API-Umfang 3.0.0: öffentlich sind 25 Namen — die 18 oben plus sitzungen, sitzungWaehlen, sitzungLoeschen, ergebnisEntfernen. migrierenStand (SPL-10) ist interner Helfer und wird nur von Spiel.laden() aufgerufen, nicht von der OberflĂ€che; ihn fĂŒhren Doku/Test als „intern“.

Diagnose-Aggregat (KLR-05a, 31.10.2026). Anonyme ZĂ€hler der gelesenen Hilfecodes je „Schritt" (= erfĂŒllte Ziele):

AufrufRĂŒckgabeVerhalten
diagnoseZaehlen(code){ok:true, schritt, anzahl} \{fehler, grund}liest einen H-Code ĂŒber hilfeLesen und zĂ€hlt ihn in der Sitzung des Codes (auch wenn eine andere aktiv ist, analog ergebnisEintragen). Unbekannte Sitzung → {fehler:"sitzung"}; unlesbarer Code → bestehende Codec-Fehlerklassen. Wiederholtes Einlesen zĂ€hlt erneut (bewusste Grenze, kein Personenbezug, keine Dedup-Daten).
diagnose(){plaetze, gesamt, schritte:[{schritt, anzahl}]}anonymes Ziel-Histogramm der aktiven Sitzung; plaetze ist der Nenner fĂŒr „Schritt k: n von 20“, schritte aufsteigend, nur beobachtete Schritte. Keine Einzelzuordnung, kein Personenbezug.

Abnahme: tests/klassen-diagnose.test.js — 20 H-Codes (5×Schritt 1, 7×Schritt 2, 8×Schritt 3) → ZĂ€hlung 5/7/8, gesamt 20; Gegenprobe: ein eingeschmuggeltes Personenfeld (name/platz) macht rot; keine Einzelzuordnung im Speicher. Kein neues Bedienelement (R12 bleibt 6; Anzeige = KLR-05b).

20-Übersicht (KLR-08a, 31.10.2026) — ergĂ€nzt die Tabelle:

AufrufRĂŒckgabeVerhalten
uebersicht()null \{plaetze, eingerichtet, dauerMin, erstellt, abgegeben:[platz], offen:[platz], fertig, offenAnzahl, ohnePlatz, restMinuten, abgelaufen}neu 3.0.0 (KLR-08a): reine Daten der aktiven Sitzung, keine Namen. abgegeben = PlĂ€tze mit Ergebnis (aufsteigend); offen = PlĂ€tze 1..plaetze ohne Ergebnis (aufsteigend); fertig/offenAnzahl = LĂ€ngen; ohnePlatz = EintrĂ€ge mit Platz 0 oder > plaetze; restMinuten = max(0, ceil((erstellt + dauerMin·60000 − jetzt())/60000)), abgelaufen = restMinuten 0. plaetze:0 → eingerichtet:false, offen:[], offenAnzahl:0 (kein erfundener Nenner); ohne aktive Sitzung null.

erzeugen({
}) erbt die KlassenstĂ€rke: fehlt plaetze beim Anlegen, ĂŒbernimmt die neue Sitzung die KlassengrĂ¶ĂŸe der bisher aktiven Sitzung (sichtbar statt still 0); ein ausdrĂŒckliches plaetze (auch 0) gewinnt.

Abnahme: tests/klassen-uebersicht.test.js — plaetze 20, Ergebnisse auf 1/2/5, T+2 min bei dauerMin 10 → fertig 3, offenAnzahl 17, offen 3/4/6..20, restMinuten 8; GrenzfĂ€lle plaetze 0, alle 20 abgegeben, abgelaufen, keine Sitzung; KlassenstĂ€rke-Erbe getestet.

Wortwahl im Klassenraum (3.0.0, verbindlich): Die Bestands-Tafel der Ampel behĂ€lt „Platz N“ (Platz N · Sterne · Zeit — B-Vertrag/O1); alle neuen 3.0-FlĂ€chen nutzen „Sitzplatz N“, und das Wort „Abgabe“ gilt einheitlich in neuen Flows (Sitzungsliste, Dialoge, Übersicht, Diagnose). Die 20-Übersicht und die Ampel nutzen eine Definition: „fertig/abgegeben“ zĂ€hlt nur gĂŒltige PlĂ€tze 1..plaetze, Ergebnisse mit ungĂŒltigem Platz werden separat als „Abgaben ohne gĂŒltigen Platz“ ausgewiesen (KLR-10).

Öffnungsweg (verbindlich). Ein neuer Auftrag entsteht nur ĂŒber Spiel.instanzErstellen({ticketId | gen:{skill, seed}, seed, quelle:"klassenraum", ohneFlow:true}) — vorher prĂŒfen, denn der Aufruf wirft bei unbekanntem Ticket. ohneFlow: true ist Pflicht, sonst macht der lokale Flow-Regler denselben Code auf zwei GerĂ€ten verschieden. Eine Klassenraum-Instanz belegt keinen Postfach-Platz und ist im Postfach unsichtbar (Entscheidung E1).

12.3 Fehlerformen

Alle Fehler sind Daten, keine Ausnahmen: {fehler:<Klasse>, grund:<Satz>}. Verbindliche Klassen (Stand 2.9.0, gemessen im Code): lĂ€nge · zeichen · prĂŒfziffer · auftrag · fassung · sitzung · wahl · format · abnahme · bereich. null nur aus ausCode/ergebnisLesen, wenn gar nichts eingegeben wurde. Der Satz ist Anzeigetext und gehört der OberflĂ€che; wörtlich vorgegeben ist nur „PrĂŒfziffer stimmt nicht — hast du ein O statt 0 getippt?".

12.4 Der Netzkennwert (Messform)

Der Determinismus wird gemessen, nicht behauptet: Spiel.klassenraum.netzkennwert(netz) liefert 6 Zeichen aus dem Alphabet, FNV-1a-32 ĂŒber das kanonische Netz (geraete und kabel sortiert, netz.zustand bleibt draußen), höchstwertige 5 Bit zuerst. Gleicher Abdruck = derselbe Auftrag; ungleicher Abdruck = Alarm, dann ist der Seed nicht vollstĂ€ndig im Code gelandet. GeprĂŒft in tests/klassenraum-determinismus.test.js (zwei getrennte Prozesse, zeichengleich).

12.5 Hilfecode H-XXXX-XX

Der Azubi kann der Lehrkraft sagen, wo er hĂ€ngt, ohne Server, Konto oder Bewertung. Form: 9 Zeichen — H + Bindestrich + 4 Nutzzeichen + Bindestrich + 2 PrĂŒfzeichen; dasselbe 32-Zeichen-Alphabet und dieselben PrĂŒfzeichen wie oben. Nutzteil 20 Bit: sitzung 5 (1..31) · platz 5 (0..31) · schritt 5 (erfĂŒllte Ziele) · offen 5 (offene Ziele). UngĂŒltige Bitlage: sitzung = 0 (erzeugt der Encoder nie). Die Lehrkraft tippt den Code in ihr bestehendes Feld „Ergebnis-Codes" (mehrere Codes passen hinein) — kein neues Bedienelement, R12 bleibt bei sechs sichtbaren Elementen (Leitungs­entscheidung O1). Der Hilfecode ist kein Bewertungssignal; er beschreibt nur Ziele, die schon erfĂŒllt bzw. offen sind.

12.6 Lokaler Server (Stufe C) — 3.1, im Produkt nicht gebaut

Ehrlicher Stand 2.9.0: shell/src-tauri enthĂ€lt keinen Server, src/ enthĂ€lt keinen QR; der Schalter „Klassenraum-Server" in den Einstellungen hat deshalb keine Wirkung und wird in 3.0 entweder entfernt oder als „nicht in dieser Fassung" umtextet (VIS-04). Die Bauform der Stufe C steht fest und ist belegt: C1 in der Desktop-.exe (Rust), C2 als eigenstĂ€ndiges Binary in tools/klassenraum (eigenes Cargo-Projekt, cargo build --release, cargo test 10/10, pwsh -File tools\klassenraum-probe\C-http-probe.ps1 20/20 — selbst gemessen am 31.10.2026 laut docs/STAND.md/AGENTS.md). Beide Endpunkte, nur lokal, keine Klarnamen, Standard aus:

Der Einbau von C1 ist nicht 3.0-Bestandteil: er braucht eine gemessene 2-Rechner-Umgebung (KLR-06 → 3.1). Ohne Server bleibt die Klasse arbeitsfĂ€hig (Codes eintippen).

12.7 QR-Code (Stufe D) — 3.1, im Produkt nicht gebaut

Inhalt wĂ€re genau der Auftragscode — kein Link, keine Adresse, kein Konto, damit jeder handelsĂŒbliche Scanner funktioniert; die Desktop-Fassung wĂŒrde das Bild in Rust erzeugen und als Data-URI an die OberflĂ€che reichen. In der Browser-Fassung bleibt Abtippen der Normalfall. Der Wiki-Text darf keinen gebauten QR behaupten (VIS-04); die UI folgt erst nach dem JS-Port (KLR-07 → 3.1).

12.8 Grenzen (bewusst) und 3.0/3.1-Schnitt

GrenzeFestlegungWo belegt
Adressierbare AuftrĂ€ge58 von 76 Handtickets — der Auftragscode hat 6 Bit (0..63); die 2.1-Tickets sind nicht klassenraum-adressierbar, der Formatwechsel kommt erst 3.1 mit Migrationswegsrc/spiel/klassenraum.js TABELLE_AUFTRAEGE, docs/STAND.md Ausnahme „Klassenraum-Codec"
Fertigkeiten30 in DATEN.skills, davon 28 mit Injektor; lab.ipv6/lab.wlan stehen ehrlich auf tauglich:false (inhaltlich geĂŒbt, nicht paketweise simuliert)TABELLE_SKILLS, § 17.1
Sitzungen (3.0.0)mehrere Sitzungen je GerÀt, eine aktiv; keine still ersetzte Sitzung mit Ergebnissen (KLR-01a/b); Sitzungskennung 1..31, PlÀtze 0..31dieser Abschnitt, § 12.1/§ 12.2
Kein Schutz gegen Abschreibenwer den Code hat, hat den Auftrag — gewollt—
Keine IdentitĂ€t, keine Zeit-/Sperrlogik, keine Serverpflicht, kein QR im Browser, keine Auswertung ĂŒber die Ampel hinaus2.x/3.0D § 8
3.1C1-Server, QR-UI, Codec-Formatwechsel mit Migration, Diagnose bleibt anonym (§ KLR-05 in docs/entwicklung/Konzept – 3.0.md)Konzept 3.0 §5

Abgrenzung zu Ă€lteren Dokumenten. docs/entwicklung/Klassenraum/ A–D bleiben Protokolle; wo sie sich widersprechen, gilt dieser Abschnitt (Leitungsentscheidungen L1–L7 und O1 vom 09.10.2026). Insbesondere: E1 (unsichtbar im Postfach), keine Bewertung, plaetze gehört zur Sitzung, R12 misst je Ansicht und wird fĂŒr die Klassenraum-Ansichten nicht aufgeweicht.


13 · Hilfestellung — Stufen und Schnittstellen (07.10.2026) — verbindlicher Vertrag

Wiederhergestellt 31.10.2026. VollstĂ€ndiger Vertragstext: entwicklung/Hilfestellung – Stufen und Schnittstellen.md; bei Widerspruch gilt der Vertragstext. Dieser Abschnitt hĂ€lt fest, was fĂŒr den ganzen Bau gilt.

Anlass (Auftrag des Nutzers, 07.10.2026). Ein Azubi kann die Syntaxen nicht kennen, hat im Spielflow aber keinen Zugriff auf eine Lernnotiz. Die Hilfe muss dorthin, wo er steht: ins Terminal, in die Leiste und in einen Trainingsbereich abseits der AuftrĂ€ge — schrittweise mit dem Bildungsstand wachsend.

13.1 Die drei Achsen (nicht verwechseln)

AchseFeldWerteBedeutung
BildungsstandSpiel.einst.stufeazubi \azubi-plus \geselle \meisterVoreinstellung des Menschen; wird nie automatisch geÀndert
ErklĂ€rtiefeSpiel.einst.niveauE \AP1 \AP2Wie ausfĂŒhrlich Konsole und Simulation erklĂ€ren
PrĂŒfungsstrengeSpiel.einst.wahl, inst.niveauauto \E \AP1 \AP2Wie streng ein einzelnes Ticket bewertet wird

Spiel.stufe.setzen(id) zieht einst.niveau nach, lÀsst es aber einzeln verstellbar.

13.2 Neue und geÀnderte Datenformen

GegenstandFestlegung
RangSpiel.stufe.rang(id) → 1
4; verglichen wird immer ĂŒber den Rang, nie ĂŒber den Namen; RĂŒckfall azubi (kein Wurf)
Hilfekonto je TicketSpiel.HILFE_KONTO = {azubi:6, "azubi-plus":4, geselle:2, meister:0}; gezĂ€hlt je Ticket ĂŒber Spiel.stufe.hilfeZiehen(inst); ein leerer Vorrat sperrt nicht — die nĂ€chste Sprosse kostet Sterne
VorschlÀgeDATEN.hilfen.VORSCHLAEGE; bereich ist immer eine Spiel.LEITER[].id (link, vlan, ip, gateway, route, dienst); art ∈ {pruefen, aendern}
EntscheidungSpiel.hilfe.passend/geruest/leiter/syntaxBruecke (nur angefĂŒgt); die OberflĂ€che entscheidet nichts selbst
FehlertexteSpiel.fehlertext({netz, id, modus, art, eingabe, ausgabe, fehler}), Tiefe aus Spiel.stufe.text(...)
Mini-AnkerSpiel.mini.hilfe(id) (Denkhilfe, nie die Lösung) und Spiel.mini.anker(id) → {titel, text, quelle, wiki}
TrainingSpiel.st.training = {je: {[szenarioId]: {versucht, bestanden, bestes}}} — Spielstand v:3; Szenarien DATEN.trainings, Logik Spiel.training

13.3 Verhalten, das vertraglich feststeht

  1. Training zahlt nichts. Kein Geld, kein Ruf, kein Karrierefortschritt, kein Eintrag in st.erledigt, keine Wochenwertung, kein Postfach; es zĂ€hlt nur fĂŒr den Lernmotor (L.ueben) und den Trainingsstand. Durchgesetzt an einer Stelle: erste Zeile von Spiel.abschliessen leitet quelle === "training" um.
  2. quelle: "training" ist unsichtbar. Spiel.postfach() filtert sie heraus (wie pruefung/raetsel).
  3. Die Hilfe ist schrittweise. Je Stufe 0/1/2 VorschlÀge (meister 0), Denkhilfe bei jedem Mini (azubi/azubi-plus), nach falscher Antwort (geselle), gar nicht (meister).
  4. Nichts wird ungefragt verraten. Spiel.mini.hilfe liefert nie den Lösungstext; ein Test prĂŒft das gegen Spiel.mini.loesungText.
  5. Kein Baustein ohne Aufruf. Jede FlĂ€che wird aus dem echten Weg gerufen (Terminal-Zeile, Leisten-Knopf, Ansicht) — grĂŒne Tests ohne Aufruf gelten als nicht fertig.

15 · Nachtrag des Nutzers: Wiederholungssperre, FĂŒhrung, generierte Fragen (09.10.2026) — Vertrag

Wiederhergestellt am 31.10.2026 fĂŒr 3.0.0.

Anlass. Nach dem Fahrplan-Auftrag verlangte der Nutzer vier Dinge: (1) AuftrĂ€ge dĂŒrfen sich in einer Sitzung nicht schnell wiederholen, (2) im Labor soll bei Bedarf der nĂ€chste nötige Schritt gezeigt werden („pulsiert oder ein Pfeil darauf zeigt"), (3) Fragen sollen möglichst immer neu generiert werden, (4) alles bis zum Production Release ausarbeiten.

15.1 Neue und geÀnderte Datenformen

GegenstandFestlegung
WiederholungssperreSpiel.st.mini trĂ€gt zug (Zugnummer) und gespielt (Mini-id → Zugnummer); zuletzt bleibt das Sperrfenster (25). Pro Fertigkeit kommt jedes Mini einmal, danach das am lĂ€ngsten nicht gespielte. Alte/beschĂ€digte StĂ€nde werden rekonstruiert, nie geworfen
MischungSpiel.MINI.GENERIERT = 3 — jede dritte Frage darf generiert sein
Generierte FragenDATEN.fragenVorlagen (11 Vorlagen) und DATEN.fragenDenkfehler (20 Arten); Spiel.fragen.erzeuge/erzeugeVoll/fuerSkill/merken/von/ausId/alle; Id gf-<vorlage>-<seed>, aus sich selbst wiederherstellbar; generierte Fragen stehen nicht in Spiel.mini.alle()
NĂ€chster SchrittSpiel.naechster(inst) → {geraet, ziel, text, bereich} oder ehrlich null; dazu Spiel.naechsterBedarf/-Wartezeit/-Wann/-Stufe, FUEHRUNG_NACH_MS 90 s, FUEHRUNG_ZEIGEN_MS 15 s, NAECHSTER_WANN (azubi/azubi-plus „nachzeit", geselle „nachfehler", meister „nein")
Vierte Quellequelle: "klassenraum" ist aus Postfach, Offen-ZĂ€hler und „Heute"-Kachel ausgenommen (wie training) und nimmt am Ereignistakt nicht teil

15.2 Verhalten, das vertraglich feststeht

  1. Rotation statt Filter. Jedes Mini einer Fertigkeit kommt einmal dran, bevor sich eines wiederholt; bei echter Erschöpfung das am lĂ€ngsten nicht gespielte. Alles deterministisch ĂŒber Zufall — kein Math.random, kein Datum.
  2. Die FĂŒhrung verrĂ€t nichts. Der Statustext der Simulation ist die Lösung; bei Netz-Zielen zeigt der Fingerzeig deshalb nur GerĂ€tename und Zieltext, nie die Ursache. Er erscheint nur auf Bedarf (Knopfdruck oder 90 s ohne Fortschritt), einmal je Stillstand, und verschwindet, sobald der Schritt getan ist; meister bekommt sie nie ungefragt.
  3. Generierte Fragen sind bewiesen richtig. Die Vorlage rechnet ihre Lösung selbst, der Generator lĂ€sst nur Fragen durch, bei denen genau eine Option besteht; der Test rechnet mit eigener Arithmetik nach und prĂŒft jede falsche Option gegen die dokumentierte Denkfehler-Menge.
  4. § 13.3.1 gilt auch ĂŒber den Ereignisweg. Der 20-Sekunden-Ereignistakt nimmt training und klassenraum aus — vorher zahlten beide ĂŒber weiterempfehlung Ruf und legten neue Tickets an.
  5. Die Wiki zeigt nur, was das Spiel kann — oder kennzeichnet den Befehl ausdrĂŒcklich als echtes GerĂ€t („Echtes IOS, nicht das Labor"). Ein WĂ€chter fĂŒhrt jede Rezeptzeile mit Prompt in der echten CLI aus.

16 · Welle 1 (2.1 + 3.0-GerĂŒst) — verbindlicher Vertrag (26.10.2026)

Wiederhergestellt am 31.10.2026 fĂŒr 3.0.0.

Anlass. Diese Session baut das Maximal-Programm 3.0 in Wellen. Welle 1 umfasst 2.1 S1/S2 (Wahrheit, Korrektheit) sowie parallel UX-P1 und das Werkstatt-GerĂŒst. Damit Code und Doku nicht auseinanderlaufen, steht hier zuerst die Datenform.

16.1 Eine Zahlenquelle: docs/STAND.md (generiert)

GegenstandFestlegung
Erzeugertools/stand.py erzeugt docs/STAND.md und einen markierten Block in README.md; Quelle sind Messungen (Testlauf, Modulliste des Baus, Dateisystem ort), keine getippten Zahlen
Blockmarker<!-- STAND:ANFANG --> 
 <!-- STAND:ENDE -->; Handarbeit in diesem Bereich wird ĂŒberschrieben
PrĂŒfmoduspython tools/stand.py --pruefen vergleicht erzeugten Text mit den Dateien; Abweichung → Exit 1 mit betroffener Zeile
PflichtfelderFassung, Baukennung, Tests (grĂŒn/gesamt/ĂŒbersprungen), Testdateien, headless Module, Bau-Module, src/-Dateien und -Zeilen (Methode benannt), Einzeldatei Bytes + SHA256, Inhalte (Tickets, Minis, Injektoren, Trainings, Wiki-Seiten, Fertigkeiten), docs/doku-Seiten
GeltungREADME, Liesmich, AGENTS, Bauen, SITZUNGSABSCHLUSS und CHANGELOG nennen diese Zahlen nur noch ĂŒber STAND.md; historische Abschnitte bleiben unangetastet
R12-MessungRohmessung Nachweise/rauch/r12.json (rauch.py --r12, je Breite × Ansicht, gemessen:false + Grund statt 0); verbindliche Datei Nachweise/3.0/r12.json (ethos.py --dom --r12); eine Zahlenquelle ist die Zeile „R12 Bedienelemente“ in docs/STAND.md; Norm = gemessene Ratsche: Anzahl VerstĂ¶ĂŸe ĂŒber der Grenze aus tests/stil-stand.json, Zuwachs = ROT

16.2 Skip-Gate (2.1 S1)

tests/run.js endet mit Exit 1, wenn uebersprungen(ergebnisse) > 0. Ein Test ohne Zusicherung gilt damit als rot, nicht als grĂŒn. tools/test.sh reicht den Exit-Code unverĂ€ndert durch; die Bilanzzeile bleibt im Text. Die Definition â€žĂŒbersprungen = 0 Zusicherungen" (tests/harness.js) bleibt unverĂ€ndert.

16.3 TTL und Probe-Art je Betriebssystem (2.1 S2)

GegenstandFestlegung
TTL-Startwertesrc/sim/l3.js: Windows-Ziele 128, Linux-Ziele (Server/NAS mit Linux-Betriebssystem) 64. Genau ein Startwert je EndgerĂ€tetyp (PC/Server/NAS); Router, Switch und Internet fĂŒhren ihre eigenen TTL-Konstanten in ihren Modulen
TracerouteWindows-EndgerĂ€te senden ICMP-Echo; Linux-EndgerĂ€te senden UDP-Proben auf hohe Ports, wie im Wiki gelehrt. Die sichtbare Ausgabe bleibt gleich; nur Sim-GrĂŒnde dĂŒrfen abweichen
Referenzstandtests/sim-stand.json wird nach der Änderung bewusst neu erzeugt; die Abweichung wird im CHANGELOG begrĂŒndet. Danach ist node tools/sim-stand.js grĂŒn

16.4 Design-Tokens und Bewegung (UX-P1)

GegenstandFestlegung
Dauer-Tokens--dauer-kurz:80ms · --dauer:160ms · --dauer-mittel:240ms · --dauer-lang:320ms; ZÀhler --dauer-zaehler:700ms; Easings --ease-standard, --ease-out, --ease-pop
JS-Zugriffgenau eine Funktion UI.zeit() liest Tokens per getComputedStyle; Animations-/Wartepfade in src/ui/ enthalten keine ms-Literale mehr. SCHRITT_MS bleibt Datenkonstante, wird aber an das Tempo gekoppelt
Lesbarkeitalle Text-Tokens ≄ 4,5:1 auf --bg/--panel/--panel-2 in beiden Schemata
Pressgenau eine .knopf:active-Transform-Regel; Druck 80 ms
IconsUI-Chrome (Knöpfe, Überschriften, Chips) nutzt UI.symbol; Emoji nur in Klartext-Copy außerhalb der Chrome

16.5 Werkstatt-GerĂŒst (3.0, Welle 1)

Wichtig: Ein freies Labor existiert bereits (UI.sandbox in src/ui/start.js, Store "sandbox", v:1, Autosave bei netz-geaendert, erreichbar ĂŒber „Freies Labor"). Die Werkstatt erweitert diesen Weg, sie erfindet keinen zweiten.

GegenstandFestlegung
DatenformSpeicher "sandbox" bleibt {v:1, netz}; neue optionale Felder (name, vorlage) sind erlaubt, alte StĂ€nde tolerieren sie (nie werfen). Die Karriere ("labor") wird nie berĂŒhrt
NetzformunverÀndert {geraete:{id:{
}}, kabel:[
]} wie UI.labor.laden(netz, 
); keine neuen GerÀtetypen in Welle 1
Modulesrc/spiel/werkstatt.js → Spiel.werkstatt (reine Logik), src/ui/werkstatt.js → UI.werkstatt (GerĂŒst), src/stil/werkstatt.css; bauen.py lĂ€dt src/spiel/*.js und src/ui/*.js alphabetisch
Editor-MengenAuswahl-Erweiterung ausschließlich UI-Zustand: Z.auswahl bleibt fĂŒr EinzelgerĂ€t; neu Z.auswahlSet + UI.labor.auswahlMehr(); Duplizieren erzeugt einen Verlaufsschritt, Namen mit Suffix -2/-3, Kabel werden nicht mitdupliziert
Minimapreine Ansicht (SVG-Vorschau), keine Datenform, kein Speicher
Wirkung vor GrĂŒnDas GerĂŒst zĂ€hlt erst als gebaut, wenn es aus dem echten Weg erreichbar ist (UI.sandbox.laden() → Werkstatt-Leiste, Beleg tests/ui-werkstatt.test.js)
Nicht in Welle 1Vorlagen-Inhalte, Challenge-Karten, PDU/PCAP, Export, neue GerÀtetypen, Zonen

16.6 Schreibrechte Welle 1 (historischer Auszug)

Ein Schreiber je Datei; die damalige Aufteilung steht im Git-Stand 9716b08^. FĂŒr 3.0.0 gilt die 10-Owner-Ordnung aus AGENTS.md A1.

17 · Welle 2+3 (2.1 S3–S8) — verbindlicher Vertrag (29.10.2026, Stand 2.9.0)

Wiederhergestellt am 31.10.2026 fĂŒr 3.0.0 (Commit 9716b08 hatte § 10–§ 18 gelöscht). Der Abschnitt beschreibt den Vertrag der Welle 2+3; Zahlen stehen ausschließlich in docs/STAND.md, historische Sollzahlen sind als solche benannt.

Anlass. Nach Welle 1 (S1 Wahrheit, S2 Korrektheit) wurde 2.1 zu Ende gebaut: S3 PrĂŒfungsnĂ€he/Generator, S4 Hilfeleiter/Doku-Ziele, S5 Lehrtexte/Fragen/Dichte, S6a/S6b IPv6/Betrieb/WLAN, S7 Volumen/Terminologie, S8 Fassung 2.1.0. Es gilt weiter: ein Schreiber je Datei, keine halbfertigen BeitrĂ€ge, Wirkung vor GrĂŒn.

17.1 Drei neue Fertigkeiten (S6a/S6b) — append-only

IndexidNameapPflichtinhalte
27lab.ipv6IPv6: Adressen, PrĂ€fixe, NDPAP1Wiki-Seite (≄4 Abschnitte, Quellen), Lehrtext E/AP1/AP2, 6 Minis, 1 Ticket, 1 Hilfe, Senior-Frage, PrĂŒfungsfrage, ≄1 Injektor; kein Training (namentliche Ausnahme, docs/STAND.md)
28lab.betriebBetrieb und Überwachung (Syslog, Schwellwert, Backup, Uptime)AP2wie oben, 6 Minis, 1 Training, 1 Ticket, 1 Hilfe, ≄1 Injektor
29lab.wlanWLAN: SSID, VLAN, Sicherheit, GastnetzAP1wie oben, 8 Minis, 2 Tickets, 1 Hilfe, ≄2 Injektoren; kein Training (namentliche Ausnahme, docs/STAND.md)

17.2 Generator, PrĂŒfung, Abnahme (S3)

GegenstandFestlegung
Generierte TicketssymptomText/briefingText liefern vier Bausteine (Anlass mit Zeit · Beobachtung in Kundensprache · Folge · was schon versucht wurde), 180–420 Zeichen, 2–4 SĂ€tze, 0 Ursachenwörter (docs/entwicklung/Stilfaden – Auftragstexte.md), Titel 15–46 Zeichen, deterministisch ĂŒber Zufall (kein Math.random, kein Datum)
Stil-Gatetools/texte-messen.js --pruefen prĂŒft 100 Seeds je Form (PrĂŒfung, TagesrĂ€tsel, Postfach) und die Handtickets; Exit 1 bei Verstoß
PrĂŒfungAlle 30 Fertigkeiten erreichbar (auch Stufe 6 und lab.portsec); Blueprint je AP1/AP2 (Fertigkeitssets), 3 Aufgaben/25 min und IHK-NotenschlĂŒssel unverĂ€ndert
Abnahme-RegressionBasis-Paare zusĂ€tzlich zu ICMP: Dienste (HTTP, DNS, DHCP, Druckfreigabe/Port 445) und Drucker-EndgerĂ€te; mindestens 5 Dienst-/Drucker-FĂ€lle in den Tests; REGRESSION_MAX darf dafĂŒr angehoben werden; keine neue Datenform

PrĂŒfung ist hilfefrei (LRN-01, 3.0.0). Spiel.pruefung.hilfeGesperrt(inst, optionen) ist wahr, wenn optionen.pruefung === true, wenn inst.quelle === "pruefung", oder wenn Spiel.pruefung.aktiv() einen laufenden PrĂŒfungstag liefert. Dann liefern die vier Hilfe-FlĂ€chen leere/null-Werte: Spiel.hilfe.passend → [], Spiel.hilfe.leiter → [], Spiel.hilfe.geruest → null, Spiel.hilfe.syntaxBruecke → null; die Umwege ebenfalls: Spiel.hilfe(inst) → {fehler:"prĂŒfung", grund}, Spiel.vorfuehren(inst) → [], Spiel.vorfuehrenSchritt → null, Spiel.seniorFaellig → false, Spiel.hilfe.anfaengerSchritte → []. Außerhalb der PrĂŒfung bleibt alles unverĂ€ndert; die OberflĂ€che darf zusĂ€tzlich {pruefung:true} mitgeben (Konsole/UI setzen das Flag aus Spiel.pruefung.aktiv()). Abnahme: tests/spiel-pruefung-hilfe.test.js — Gegenprobe: Hilfe-Aufrufe in der PrĂŒfung → leer/Fehler; dieselben Aufrufe ohne PrĂŒfungszustand unverĂ€ndert (nicht leer); Flag pruefung:true sperrt auch ohne laufende PrĂŒfung. IHK-Note exakt, ohne Rundung (LRN-03, 3.0.0). Der NotenschlĂŒssel vergleicht exakt: note(p) = 1 ab p≄92, 2 ab 81, 3 ab 67, 4 ab 50, 5 ab 30, sonst 6 — note(91.99)=2, note(92)=1, note(80.5)=3, note(66.9)=4. Spiel.pruefung.abgeben() speichert den exakten Prozentwert (kein Math.round); gerundet wird nur fĂŒr die Anzeige. Neu: Spiel.pruefung.bestanden(p) = p >= PRUEFUNG.BESTANDEN_AB (exakt: 49,99 % ist nicht bestanden). Kombinationstest 3 Aufgaben × 1–6 Ziele: 0 FĂ€lle unter 92 % mit Note 1. Abnahme: tests/pruefung-note.test.js (GrenzfĂ€lle exakt, Kombinationstest, bestanden-Grenze) + bestehende tests/pruefung.test.js bleibt grĂŒn. Fehlende PrĂŒfungsaufgabe zĂ€hlt 0 (LRN-04, 3.0.0). Verschwindet eine PrĂŒfungsinstanz vor der Abgabe (z. B. beim Laden bereinigt), wird sie nicht ĂŒbersprungen: Spiel.pruefung.abgeben() legt einen Teil mit {fehlt:true, anteil:0, bestanden:false} an; der Nenner bleibt fest Spiel.PRUEFUNG.AUFGABEN (3), nicht teile.length. Folge: 2 von 3 gelöst → 66,67 Punkte (Note 4), nicht 100/Note 1; voll gelöst bleibt 100/Note 1. Abnahme: tests/pruefung-abgeben.test.js — voll gelöst 3 Teile/100/Note 1; eine Instanz entfernt → teile.length 3, fehlt:true, punkte ≀ 67, Note 4 (Gegenprobe: altes Überspringen wĂ€re 100/Note 1).

17.3 Hilfeleiter und Doku-Ziele (S4)

GegenstandFestlegung
SENIOR_FRAGENSchlĂŒssel = DATEN.skills-id; 30/30 vorhanden
Stufe 5Ohne eigenes hilfen-Feld kein Lösungstext-Fallback: nur Handlungshinweise („PrĂŒfe zuerst, ob 
"); Stufe 6 bleibt „Lösung vorfĂŒhren"
Doku-ZielVorhandene Formen notiz (≄ 20 Zeichen) / tabelle / audit wiederverwenden; die 17 Briefings, die Protokoll/Wartungsbuch fordern, bekommen ein Ziel; kein neuer Zieltyp, keine neue Datenform
Stufen 5/6≄ 10 / ≄ 8 Handtickets mit echten Themen (Firewall/Log, NAS-Rechte/Backup, Port-Security in der Halle, 
); alle Texte im Stilfaden, Symptom ohne Ursachenwörter

17.4 Lehrtexte, Fragen, Dichte (S5)

17.5 Volumen, Welle 3 (S7)

Sollzahlen der Welle 3 (Summenprobe): Tickets 76 · Minis 150 · Trainings 52 · Injektoren 55 · jede Fertigkeit ≄3 Injektor-Zuordnungen · Hilfe-VorschlĂ€ge 47 · Wiki 37 · Fragen 20/15 · Lehrtexte 46 · Doku-Ziele 17/17. Der gemessene Stand steht ausschließlich in docs/STAND.md. Dazu: Terminologie (Karriere-Stufe vs. ErklĂ€rtiefe vs. Niveau; „Broadcast-DomĂ€ne" einheitlich) dokumentiert; 5 Wiki-Vertiefungsseiten (VPN/IPsec, TLS/Zertifikate, 802.1X, Backup, Virtualisierung) mit ≄4 Abschnitten und Quellen; unabhĂ€ngiger Faktencheck 100 Aussagen = 0 Fehler oder benannte Ausnahmen. Jede Einheit mit Lernziel, Quelle, Test.

17.6 Fassung 2.1.0 und Auslieferung (S8) — historisch, ĂŒberholt

Welle 3 zog 2.1.0 (Trockenlauf tools/fassung-ziehen.py --neu 2.1.0, dann --setzen; VERSION_CODE von Hand ĂŒber dem abgeleiteten Wert). Seitdem sind 2.2–2.9 erschienen; die Auslieferung ist in § 23–§ 26 und docs/entwicklung/Übergabe – Stand 31.10.2026 – Runde 8.md dokumentiert. FĂŒr 3.0.0 gilt der Fassungszug in docs/SITZUNGSABSCHLUSS.md (3.0.0, VERSION_CODE 30001). Kein Commit, kein Push, keine Veröffentlichung ohne ausdrĂŒckliche Nutzerfreigabe — unverĂ€ndert.

17.7 Zahlenkopplung und Tests

Tests, die ZĂ€hlungen hart codieren, werden auf die Vertragswerte gezogen — ohne AbschwĂ€chung. Neue Tests je Owner in eigenen Dateien; nichts gelöscht; 0 ĂŒbersprungen (Skip-Gate). Die Testzahl und alle ZĂ€hlungen stehen ausschließlich in docs/STAND.md (erzeugt von tools/stand.py); docs/STAND.md wird am Ende jeder Welle neu erzeugt. Handgeschriebene Zahlen in Doku/CHANGELOG sind Befunde.

17.8 Schreibrechte Welle 2 (historisch, 29.10.2026)

Ein Schreiber je Datei; die damalige Owner-Tabelle (O1–O5 + Lead) ist im Git-Stand 9716b08^ nachlesbar (git show 9716b08^:docs/Architektur.md). FĂŒr 3.0.0 gilt die 10-Owner-Ordnung aus AGENTS.md A1 und dem PROMPT-Roster; Kollisionen werden dem Lead gemeldet statt still gelöst.

17.9 Klassenraum-Tabellen und Ticketwachstum (Entscheidung 29.10.2026)

Befund. Das Auftrags-Codec hat 6 Bit Auftragsindex (0..63, Format § 12). 2.1 wuchs von 58 auf 76 Handtickets. Die Tabelle TABELLE_AUFTRAEGE in src/spiel/klassenraum.js ist ein Literal (eingefrorene Bindung); alte Codes bleiben dadurch gĂŒltig, auch wenn neue Tickets an anderer Stelle der lebenden Reihenfolge stehen.

Entscheidung.

  1. Kein Formatwechsel in 2.1 (keine 7-Bit-Indizes). Die 6-Bit-Grenze bleibt; das Format wird erst in 3.1 angehoben (dann mit Fassungs-/Migrationsweg, vgl. § 12.8).
  2. TABELLE_AUFTRAEGE bleibt unverÀndert die 58 IDs in historischer Reihenfolge; TABELLE_AUFTRAEGE_LAENGE = 58 bleibt. Die 2.1-Handtickets sind nicht klassenraum-adressierbar; das wird im Codec-Kopf und in § 12.8 dokumentiert.
  3. Keine Umsortierung bestehender oder neuer Tickets, um eine „lebende Reihenfolge" zu erzwingen.
  4. Test-Invariante statt Gleichheit: (a) alle 58 IDs existieren genau einmal in DATEN.tickets, (b) die Tabelle ist 58 lang und duplikatfrei, (c) alte Codes bleiben stabil (Beispielcodes fĂŒr Index 55/56/57 → storage-01/02/03), (d) Fertigkeitentabelle = 30, (e) die lebende ticketReihe() darf wachsen und wird nicht mit der Codec-Tabelle gleichgesetzt.
  5. Ehrliche Grenze: 3.1 muss vor dem Klassenraum-Feldtest das Format anheben — sonst sind nur 58 von 76 AuftrĂ€gen per Code aufrufbar.

18 · Frontend-Redesign — Gestaltungsvertrag (29.10.2026) — verbindlicher Vertrag

Wiederhergestellt am 31.10.2026 fĂŒr 3.0.0. Der vollstĂ€ndige Gestaltungsvertrag mit allen Farbtabellen, Komponenten- und Shell-Maßen steht im Git-Stand 9716b08^:docs/Architektur.md und in entwicklung/Design – Spielspaß 2.0.md; fĂŒr 3.0.0 gelten die dort festgehaltenen Tokens und Regeln, ergĂ€nzt durch die Abschnitte 21–26.

Anlass. Der Nutzer hat ein „komplettes Redesign des Frontends" beauftragt: Optik + Shell neu, Bedienlogik und Ansichten bleiben. Basis ist die Bildvorlage A · „Matrix-Elegant" unter Nachweise/Prototypen-Redesign/ (nicht versioniert). Dieser Abschnitt ist die eine Quelle fĂŒr Farbtokens, Typo-Skala, Shell-Maße, Komponenten und die Zuordnung zu den Dateien — „Vertrag zuerst".

18.1 Geltung und Nicht-Ziele

GegenstandFestlegung
Gilt fĂŒralle sichtbaren FlĂ€chen des Web-/Windows-/Einzeldatei-Baus: Tokens, Typo, Shell (Kopfzeile, linke Leiste, Statusfuß), Karten, Knöpfe, Dialoge, Toasts, NetzflĂ€che, Werkzeuge, alle Ansichten
Gilt nicht fĂŒrBedienlogik, DOM-VertrĂ€ge, Datenformen, Simulation, Tests mit Verhaltenszusicherung, Inhaltstexte — diese bleiben unangetastet
Basis-VorlageA · Matrix-Elegant; B (Holo-Suite) und C (Neon-Blueprint) bleiben Stimmungshilfen, kein Vertragsbestandteil
Nicht-Zielekeine neuen Ansichten, keine neuen GerĂ€tetypen, keine neuen Inhalte/Tickets, kein Umbau der Android-Eigenheiten ĂŒber die gemeinsamen Tokens hinaus, keine Änderung an tests/sim-stand.json
Grundsatzdie Vorlage ist Stimmungsvorlage, keine 1:1-Implementierung: Mikrolabels 8–9 px werden auf ≄ 11 px gehoben (§ 18.5)
Veraltet§ 16.4 bleibt in Kraft (Bewegung, Icon-Regel, Press-Regel) und wird durch § 18.5–18.9 ergĂ€nzt; bei Widerspruch gilt § 18

18.5 Typografie

Schriften (unverĂ€ndert, in basis.css und im Projektstamm schriften/): --display: Bricolage Grotesque (600/800 — Wortmarke, H1, Kartentitel, Knöpfe), --body: Atkinson Hyperlegible (400/700 — Fließtext, Zeilen), --mono: JetBrains Mono (400/600 — Mikrolabels, IPs, Messwerte, Statuszeile). Keine neuen Schriftdateien, keine Netzschriften.

TokenpxVerwendung
--fs-111Mikrolabels (Mono, Versalien, Sperrung .08em), SVG-Beschriftung, Chips, Statuszeile — Untergrenze fĂŒr funktionale Beschriftung
--fs-212.5dichte Zeilen, Tabellen, Hinweise
--fs-314Standard-Fließtext in Karten und Formularen
--fs-416Grundtext (body)
--fs-520Kartentitel, AbschnittsĂŒberschriften
--fs-628H2/Ansichtstitel
--fs-734H1 klein / große Zahlen
--fs-844H1 der Vorlage A (44/46)

Regeln: Funktionale Beschriftung nie unter 11 px (Gate ui-typografie.test.js). Mono nur fĂŒr Daten (IPs, Ports, Messwerte, Status, Mikrolabels), nicht fĂŒr Fließtext. 13 px und 15 px sind Altlasten aus v2.1.0 — ethos.py erlaubt sie bis zur Umstellung, neu entstehen dĂŒrfen sie nicht. H1/H2 mit line-height: 1.1–1.15, text-wrap: balance; Satzbreite max. 72 Zeichen je Spalte, Karten mit Fließtext ≄ 320 px.

21 · Runde 3b — Audit-Umsetzung, Ambient und lebende Zonen (30.10.2026) — verbindlicher Vertrag

Auftrag (Nutzer, wörtlich). „dann fixe diese doch, warum hast du das nicht gleich gemacht?" (bezogen auf die 13 Audit-Signale), „Wenn ein PC aus der Zone geschoben wird muss die zone sich mit ihm verschieben", „es fehlen noch so die ganzen ambient, atmenden, pulsierenden effekte". Beleg: Nachweise/Prototypen-Runde3/audit/NOTIZ.md (13 Signale mit Messwerten) und audit-umsetzung/NOTIZ.md.

Umsetzung (Dateien, je ein Schreiber).

Gates (letzte Messung). 873/873 Tests, ethos GRÜN (R4 169, R6 7, R9 58, R10 40 — besser als Stand), klassen 0, umlaute GRÜN, Rauchtest 45/45 (letzter Lauf), starttest GRÜN. R12 nicht gemessen (--dom). Ambient im Build per CDP-Frames belegt (16,5 % Pixel sichtbar verĂ€ndert; „Bewegung aus" = 0 Animationen).

22 · Bento 2.5 „Arbeitsplatz" (30.10.2026) — verbindlicher Gestaltungsvertrag

Auftrag (Nutzer, sinngemĂ€ĂŸ). Postfach „lieblos und unpersönlich", Kunden „zu kindlich statt Job-Simulation mit Kundenprofil", Dashboard „einzelnes unordentliches Objekt" (Ziele, Sitzung, Klassenraum), Training „Kaskade an Einheiten" und „altes Design", minimierte Leiste „komplett ĂŒberarbeiten: viel interaktiver, bessere und innovativere Inhalte"; dazu der Hilfeleiter-Slogan aus dem ersten Screenshot. Ziel: eine ruhige, dichte ArbeitsoberflĂ€che eines IT-Dienstleisters in Bento-Form — echte Hierarchie, glaubwĂŒrdige Daten, wenig Farbe, keine Emoji-Deko, keine Riesen-Slogans in FunktionsflĂ€chen.

Vertrag. Vollfassung: Entwurf – Bento 2.5 (Arbeitsplatz).md. Die LeitsĂ€tze:

  1. Bedienlogik, Datenwege und Spielzustand bleiben unverĂ€ndert. Keine Änderung an src/sim/, src/modell/, src/kern/, src/cli/; Ansichten rufen dieselben Spiel.*-Wege.
  2. Test-Haken bleiben (Klassen und Bedeutung): `.sp-postfach/.sp-pf-liste/.sp-leser/.sp-mail/ .sp-brief/.sp-knoepfe/.sp-blatt/.sp-hilfe/.sp-leiter,.hb-seite,.kr-seite/.kr-kopf/ .kr-raster/.kr-kunde/.ka-block,.tr-seite/.tr-liste/.tr-karte/.tr-geruest/.tr-quelle/ .tr-ergebnis/.tr-offen/.tr-grund/.tr-lernen/.tr-fehler*`, Leisten-Haken siehe Test spiel-leiste-hilfe.test.js (.lk-zeile mit exakter Kindfolge, .lk-auf, .lk-mini, .lk-mini-hilfe, Mini-Zeichen, Hilfe-Knopf).
  3. Nur Tokens. Neue FlĂ€chen nutzen ausschließlich basis.css-Tokens. Schrift nur var(--fs-*), Abstand nur {0,4,8,12,16,24,32}px, Radius nur Tokens, keine Farb-Literale, kein !important außerhalb basis.css (Gates: tools/ethos.py, tools/klassen.py, ui-typografie, ui-symbole). Die fĂŒnf Theme-Dateien zzz-thema-*.css bleiben unangetastet; bestehende Klassennamen bleiben, damit ihre Regeln weiter greifen.
  4. Keine erfundenen Zahlen, keine Emoji in der UI-Chrome. Anzeige nur aus Spiel.*/DATEN; Kunden-Emoji (DATEN.kunden[*].symbol) wird in den KundenflÀchen durch Monogramme ersetzt.
  5. Gemeinsame Bausteine der Leitung: src/stil/bento.css (12-Spalten-Raster .bn-raster, Kachel .bn-k mit Varianten flach|akzent|iris|geld, `.bn-kopf/.bn-kicker/.bn-titel/.bn-lead/ .bn-meta/.bn-fuss/.bn-zeile/.bn-wert/.bn-chips/.bn-chip/.bn-marke/.bn-trenner/.bn-leer`, Spalten .bn-s3
s12) und src/ui/bento.js (UI.bento.monogramm/initialen/kachel/kopf).
  6. Ein Schreiber je Datei. Postfach: spiel.js + spiel.css; Kunden: karriere.js (nur Kunden/Kundenakte) + karriere.css + kundenakte.css; Dashboard: hub.js + hub.css; Training: training.js + training.css; Leiste: leiste.js + leiste.css; Grundlage und Integration: Leitung (bento.*, Doku, Werkzeuge, Nachweise).
  7. Abnahme je Bereich: private Vorschau python tools/vorschau.py --name <bereich> + python Nachweise/bento/foto.py --html vorschau-<bereich>.html --marke <bereich> (Bilder 1440/960/720), SichtprĂŒfung, mindestens zwei Iterationen; node tests/run.js grĂŒn, klassen.py 0, ethos.py GRÜN (R4/R9/R10 dĂŒrfen nicht steigen), umlaute.py GRÜN.
  8. Nicht Teil des Umbaus: neue Ansichten, Änderungen an Simulation/Netz/Codec/Spielstand, Themes, Fassung, Push/Release. Die Leitung committet, zieht Doku/Stand nach und legt die Abnahme vor.

Stand (30.10.2026). Umgesetzt von fĂŒnf parallelen Bereichsteams (ein Schreiber je Datei), danach von der Leitung integriert und von zwei unabhĂ€ngigen PrĂŒfungen (Design; Theme/Struktur) gegengelesen. Gates: node tests/run.js 878/878 grĂŒn, 0 ĂŒbersprungen · sh tools/test.sh --rauch 45/45 · python tools/klassen.py 0 · python tools/ethos.py GRÜN (R2 20→19, R4 169→153, R9 62→58, R10 42→39) · python tools/umlaute.py GRÜN · node tools/sim-stand.js unverĂ€ndert · python tools/seite.py --pruefen GRÜN (54 Dokumente). SichtprĂŒfung je Bereich bei 1440/960/720, Theme-PrĂŒfung 7 ZustĂ€nde × 7 Ansichten ohne harten Kontrastbruch, 0 Emoji in src/ui/**/src/stil/**. Nicht gemessen: R12 (--dom), Lauf in Android, Hover-/TastaturzustĂ€nde im Vollbild. Die Windows-.exe wurde dagegen gebaut, mit tools/q-echt.py echt bedient und gestartet. Fassung 2.5.0 ist gezogen; Zweig ausbau-1.2, Tag v2.5.0 und GitHub-Release wurden am 30.10.2026 mit ausdrĂŒcklicher Nutzerfreigabe veröffentlicht. Patch 2.5.1 (31.10.2026) verkĂŒrzt den Zuklapp-Timer der Leiste von 5 s auf 1 s. Einzelheiten: Übergabe – Stand 30.10.2026 – Bento 2.5.

23 · Runde 4 — Jobdoku, Flugschreiber, Fenster (31.10.2026) — verbindlicher Vertrag

Auftrag (Nutzer, sinngemĂ€ĂŸ). Die noch alten FlĂ€chen (Verdacht/Akte, Lernstand, Simulator, Inspektor, Shop, Fenster, Interaktion) sollen eine eigenstĂ€ndige Bento-2.5-Sprache bekommen und zu einer glaubwĂŒrdigen Job-Simulation mit didaktischer Hilfe professionalisiert werden — kreativ neu gedacht, nicht poliert; ausgeliefert als Fassung 2.6.0 inklusive Push, Tag, Release und Live-Seite. Vollfassung: Entwurf – Runde 4 (Jobdoku & QoL).

Neue und getrennte OberflÀchen-Dateien (Phase 0, Verhalten unverÀndert).

Datenformen und Schnittstellen (nur additiv).

OberflÀche (PrÀfixe bleiben, Testhaken bleiben).

Gates (gemessen 31.10.2026). node tests/run.js 930/930 grĂŒn, 0 ĂŒbersprungen · sh tools/test.sh --rauch 45/45 · klassen.py 0 · ethos.py GRÜN (R1 23, R2 17, R4 140, R9 58, R10 42) · umlaute.py GRÜN · sim-stand.js unverĂ€ndert · Bau 138 Module. SichtprĂŒfung/Rollen: fĂŒnf Bereichs-Owner, zwei unabhĂ€ngige PrĂŒfungen (Design; Theme/Struktur/A11y), Leitung integriert, committet und veröffentlicht. Nicht gemessen: R12 (--dom), Android-APK, echte Maus im ausgelieferten Tauri-Fenster (PrĂŒfbau mit eigener Kennung, weil eine fremde Instanz lief). Einzelheiten: Übergabe – Stand 31.10.2026 – Runde 4.

24 · Runde 5 — Auftragsbogen, Inspektor, Leiste als Begleiter (31.10.2026) — verbindlicher Vertrag

Auftrag (Nutzer, sinngemĂ€ĂŸ). Nach 2.6.0 meldet der Nutzer drei Bildschirmfotos zurĂŒck: „unordentlich ĂŒberlappende Elemente", „manche Elemente sind nicht richtig ausgerichtet", die Minileiste „vermittelt visuell nicht ihren Nutzen, sondern overwhelmed", das Zusammenfallen der Leiste mĂŒsse „schöner animiert" werden, und „die AuftrĂ€ge zu simulieren im Minimodus funktioniert nicht richtig, da viel zu wenig Kontext herrscht 
 das Konzept muss umgedacht werden". Verlangt: Full Skill Usage und Agenten-SchwĂ€rme. Vollfassung: Entwurf – Runde 5 (Leiste & Auftragsmappe).

Auftragsbogen (A).

Karte (D). Knoten-Labels: Baselines Name/Rolle/IP bei ikonH+18/33/48 (15 px), Box bis +62, line-height:1.15; Decluttering (labelUeber) gilt auch fĂŒr .ger-ip; die Adresse erscheint nur bei Hover/Auswahl oder Zoom ≄ IP_ZOOM (0,85).

Inspektor (B). Kopf kompakt (Mikrolabel · Name · Rollenzeile · eine Kennzahlenzeile). Ports: 2-zeiliger Block je Port (Zeile 1 Name + Link-Chip + Modus-Chip; Zeile 2 zwei gleich breite Felder Modus/VLAN mit je einer Beschriftung, ≄44 px); die Portliste scrollt in sich, ihre Höhe wird auf ganze Portzeilen gerundet (rAF + ResizeObserver) — keine angeschnittene Zeile. Die SVI-Verwaltungskarte liegt im VLAN-Reiter.

Mini-Leiste (C) — Konzept „Begleiter". Die Leiste löst keine AuftrĂ€ge; sie begleitet:

Gates (gemessen 31.10.2026). node tests/run.js 932/932 grĂŒn, 0 ĂŒbersprungen · Rauchtest 45/45 · klassen.py 0 · ethos.py GRÜN (R1 23, R2 17, R3 4, R4 135, R9 58, R10 42; R12 nicht gemessen) · umlaute.py GRÜN · Simulation unverĂ€ndert · 138 Module. Ein Test wurde umgestellt (nicht abgeschwĂ€cht): tests/ui-leiste-ueberlappung.test.js prĂŒft die neue Mechanik (mappe-an/visibility:hidden statt Kopfraum) mit 6 statt 4 FĂ€llen. Nicht gemessen: R12 (--dom), Android-APK, echte Maus im ausgelieferten Tauri-Fenster (nur PrĂŒfbau), Screenreader.

25 · Runde 6/7 — KurzĂŒbung mit Kontext und Flugschreiber-Aufmerksamkeit (31.10.2026)

Runde 6 (KurzĂŒbung). Neues Modul src/spiel/kurzfrage.js: Spiel.kurzfrage.fuer(inst, {niveau}) → {id,titel,frage,lage:[{label,wert}],optionen:[{text,richtig}], erklaerung,ort} | null. FĂŒnf Vorlagen (netz/maske/gateway/portmodus/zustand) aus inst.netz, Modell.adressen, Sim.adresse, IP.*, g.running.ports/.if, Kabeln und g.an; deterministisch aus inst.iid (Zufall ohne Uhr); genau eine richtige Option; Niveau E/AP1/AP2; Lage 2–4 Zeilen; kein Lösungstext (Fehlerstellen nach Spiel.plan.diff ausgeklammert). Die Leiste (src/ui/leiste.js, leiste.css) rendert bei offenem Auftrag diese Frage mit Lage-Block; ohne verwertbares Netz zeigt sie keine Frage, sondern den Weg ins Labor. Das generische Mini-Ticket bleibt als KopfĂŒbung im Dialog (karriere.js/mk-einordnung).

Runde 7 (Flugschreiber). src/ui/simpanel.js + simulation.css: eine Lesespalte mit drei Ebenen — Ergebnis (Urteil 28 px, eine Zahlenzeile, Segmentleiste), Befund (erster Fehler als 2-px-Rail-Zeile, ein ErklĂ€rungssatz, leiser Weg „Zum Fundort →"), nĂ€chster Schritt (numerierte Hilfe, hĂ€ngender Einzug, leiser Verweis „GerĂ€t zeigen →"). Höchstens EIN gefĂŒllter Hauptweg je Zustand, alle ĂŒbrigen Ghost 32 px; sichtbare 44-px-TrefferflĂ€chen fĂŒr Transport/Filter bleiben unsichtbar erhalten. Hilfe an/aus als Ghost-Schalter, Format als Textsegment. Testhaken .si-*, Transport, Zeitachse, UI.simpanel.anfaengerHilfe und PDU unverĂ€ndert.

Messung (31.10.2026). Runde 6: 950/950 Tests in der abgetrennten PrĂŒfkopie; 228/228 Fragen ĂŒber 76 Handtickets × 3 Niveaus; Lösungstext-Scan 0 Treffer. Runde 7: Dock 380 px, laute Knöpfe 11→2, primĂ€re 3→0 (Fehler+Hilfe an), FlĂ€chen 4→1; 10→2/3→0/3→1 (Hilfe aus); 6→2/4→1/2→0 (Ergebnis); Überlauf 0; Mikrolabel 6,08:1. Im gemeinsamen Arbeitsbaum 1033/1033 grĂŒn (dieser Commit enthĂ€lt nur Runde-6/7-Dateien). R12 nicht gemessen.

26 · Runde 8 — Abteilung „Subnetting" (31.10.2026) — verbindlicher Vertrag

Auftrag (Nutzer, wörtlich). „NetLab braucht ein Subnetting abteil wo das intensiv beigebracht, gelehrt mit visuellen Hilfselementen und Übungsaufgaben und tools / spielchen sind nur zum lernen von subnetting und vLAN. 
 wie das am besten in die bestehenden themes integriert wird und auch wo, ich denke ein eigenes abteil wird dem gerecht. Generell sollen alle Aufgaben Inhaltlich und vom Lernschwierigkeitsgrad optimiert werden. Mache das vollstĂ€ndig fertig und melde dich wenn der build und deploy fertig ist." Vollfassung: Entwurf – Runde 7 (Abteilung Subnetting).

Neue Ansicht subnetz („Subnetting"). Eigene Registrierung ĂŒber UI.startHaken + UI.app.registrieren("subnetz", 
); Dock-Reihenfolge in app.js REIHE direkt nach „labor" (UI.app.liste() ĂŒberspringt unbekannte Namen, der Eintrag ist damit auch ohne Ansicht unschĂ€dlich). Vier Register: Lehrpfad (8 Module), Werkzeuge (6), Üben & Spielen (Generator + 3 Lernspiele), Formelsammlung. Kein Geld, kein Ruf, kein Spielstand-Feld.

Datenformen (nur additiv, headless).

Lernen und Verbuchung. Ausschließlich ĂŒber L.ueben(skill, ok, {hilfe}) / L.fehler("labor-subnetz", skill, text); Werkzeuge, die die Lösung zeigen, verbuchen {hilfe:true} (Karte steigt nicht). Module: neu < gelesen < geuebt < sicher; „sicher" = PrĂŒfrunde bestanden (≄4 von 5, ohne Hilfe); ein Fehler senkt „sicher" auf „geuebt", nie unter „gelesen". Kein Geld/Ruf.

Lernstufen (verbindlich, PrĂŒfmaßstab). E: /24–/28, letztes Oktett, 1 Schritt, hinweis immer; AP1: /20–/30, 2–3 Schritte; AP2: /16–/31, 3–5 Schritte, optional Zeitdruck. VLSM: E –, AP1 2–3 Netze, AP2 4–5 Netze inkl. Reserve. VLAN: E ein Access-Port, AP1 zwei Ports/Erreichbarkeit, AP2 Trunk/Tagging/ Native/allowed. Formulierungsregeln und die Vorher/Nachher-Liste der optimierten Bestandsaufgaben: Subnetting – Lernstufen-Matrix und Entwurf § 10.

Optimierte Bestandsaufgaben (belegt, Tests grĂŒn). vlan-netzgroesse lehrt jetzt Hosts je VLAN → PrĂ€fix statt „/24 auf n VLANs"; mini-sub-1/mini-sub-3 Stufen getauscht (Netzadresse /26 = AP1, Hosts /28 = E; Stufen-Multiset unverĂ€ndert); mini-trunk-5 mit ausdrĂŒcklichem Native-Kontext; Denkhilfen fĂŒr mini-vlan-6, mini-trunk-5, mini-rostick-4; doppelte Trainings unterscheidbar (tr-trunk-erlauben/tr-trunk-allowed, tr-netz-maske/tr-netz-nachbar); tr-subnetz-plan auf E-Niveau gestĂŒtzt (Teiltabelle) ohne die Format-Regel niveau = f(art) zu brechen.

EinfĂŒgung in die Themes. Nur Tokens aus basis.css und Bausteine bento.css (.bn-raster/.bn-k/
); eigene Klassen sn-* (Abteilung), sw-* (Werkzeuge), sg-* (Spiele). Alle 5 Zusatz-Themes + hell/dunkel/system bei 1440/960/720 geprĂŒft; VLAN-Farben --vlan-1
8 nur mit Nummer; 44-px-Ziele scopen (global .knopf = 40 px); Fokusring zentral, kein eigener Reduced-Motion-Block; keine Emoji.

EinfĂŒgung in die Navigation. Dock (REIHE, Kurzform „Subnetz" gegen den Wortumbruch bei 76 px), Befehlspalette (automatisch ĂŒber UI.app.liste(), dazu sechs Befehle „Subnetting: Werkzeug 
"), tools/rauch.py (ein Fall „Subnetting" → 48/48), Querverweis am Ende der Trainingsliste („Zur Abteilung Subnetting") und interne Verweise zu Wiki (UI.wiki.oeffnen), Labor und Training. Kein Hub-Umbau: das „Heute"-Raster hat eine feste 12-Spalten-Belegung; eine zusĂ€tzliche Kachel hĂ€tte die gemessene Bento-Ordnung gebrochen (im Entwurf § 2.5 als „nur wenn messbar Platz frei" vorgesehen, hier bewusst verworfen).

Nicht Teil der Runde: neue DATEN.skills (30 — Codec- und Dichte-Tests eingefroren), neue Tickets, Änderungen an spiel/injektoren.js/spiel/mini.js-Mechanik/Lernmotor, src/sim|modell|kern|cli.

Stand (gemessen am 31.10.2026). node tests/run.js 1049/1049 grĂŒn, 0 ĂŒbersprungen (118 Testdateien, 98 headless Module) · Rauchtest 48/48 · klassen.py 0 · ethos.py GRÜN (R1 23, R2 17, R3 4, R4 134, R5 0, R6 7, R7 0, R8 0, R9 58, R10 41, R11 0; R12 nicht gemessen) · umlaute.py GRÜN · Simulation unverĂ€ndert · Bau 143 Module (98 headless) · Einzeldatei 4.080.274 B, SHA256 49fc2fa5
, docs/index.html und Netzwerk-Labor.html byte-gleich · Doku 65 Dokumente, 0 tote Verweise, seite.py/stand.py --pruefen grĂŒn · SichtprĂŒfung 1440/960/720 × 7 Theme-ZustĂ€nde: 0 seitlich scrollende Seiten, 0 JS-Fehler, 0 KontrastverstĂ¶ĂŸe, „Bewegung aus" = 0 Animationen (3 abgeschnittene Textknoten nur in der VLSM-Balkenbeschriftung kleiner Blöcke — die Legende nennt den vollen Namen; Bit-Zellen sind die begrĂŒndete Ausnahme der 44-px-Regel). Fassung 2.9.0 (VERSION_CODE 20901), veröffentlicht mit Tag v2.9.0. Einzelheiten: docs/STAND.md, CHANGELOG, Übergabe – Stand 31.10.2026 – Runde 8 und Subnetting – Lernstufen-Matrix. Nicht gemessen: R12 (--dom), Android-APK, Screenreader, echte Maus im ausgelieferten Tauri-Fenster.

27 · Fassung 3.1.0 — Onboarding, Analyse-Leitfaden, Labor-Aktionen, Layout (01.11.2026)

Analyse-Leitfaden (Variante B). Prozessmodell ist die vollstĂ€ndige Handlung: klĂ€ren → Lage/Schichten → Hypothese → messen → eingreifen/prĂŒfen → dokumentieren/lernen. Neue Dateien: src/daten/leitfaden.js (Texte), src/spiel/leitfaden.js (Erkennung), src/ui/leitfaden.js (Panel). Schnittstelle: Spiel.leitfaden.stand(inst) → {pruefung, fortschritt, aktuell, schritte:[{id, titel, status, impuls}]}, naechsterSchritt(inst) → {id, titel, text} | null, zuruecksetzen(inst). Die Erkennung nutzt ausschließlich vorhandene echte SpielzustĂ€nde (Auftrag, angesehene Lage/GerĂ€te, Hypothese/Verdacht, Messungen, Eingriffe, Verifikation, Abnahme) und fĂŒhrt keine neuen Fortschrittsspeicher ein, soweit ableitbar. In der PrĂŒfung (Spiel.pruefung.aktiv()) liefert die Engine nur einen neutralen Status ohne Impulse; Prozesshilfe kostet keine Sterne. Das Panel ist Text (keine neuen Bedienelemente in der Labor-Ansicht).

Onboarding. src/ui/onboarding.js zeigt beim ersten Start ohne offenen Auftrag vier Schritte aus DATEN.leitfaden.tour; Zustand einst.tour = {gesehen: true}; ĂŒberspringbar (Esc), wiederholbar ĂŒber Einstellungen. Die Tour rendert als body-level Overlay (außerhalb .ansicht) und verĂ€ndert die R12-ZĂ€hlung nicht.

Labor-Aktionen. Die rechte Leiste macht die Kernaktionen sichtbar: Auftrag/Abnahme, Messen (Ping), Konsole, GerĂ€t/Inspektor, Akte/Dokumentieren, Hilfe/Leitfaden. Umsetzung durch Umgruppieren vorhandener Funktionen und body-level MenĂŒs (keine Doppelung): die rechte Leiste zeigt die Kernaktionen als Textliste (Name + TastenkĂŒrzel), ausfĂŒhrbar ĂŒber das bestehende, bereits gezĂ€hlte MenĂŒ (Kopf „Aktionen", body-level, zĂ€hlt nicht). Bei 960/720, wo Auftragszeile/⋯ nicht sichtbar sind, öffnet ein einzelner sichtbarer Kopfknopf dasselbe MenĂŒ; das erhöht die R12-Zahl der Labor-Ansicht bei diesen beiden Breiten um genau +1 (11→12, 7→8). Diese Ausnahme ist Lead-freigegeben und wird ĂŒber das dokumentierte Baseline-Update-Verfahren (Nachweise/3.1/gates/02-
) mit BegrĂŒndung „Kernaktionen sichtbar/erreichbar", alt→neu-Protokoll und Reviewer-SĂ€tzen (r-design, r-arch) eingefroren; alle ĂŒbrigen R12-Werte bleiben unverĂ€ndert, klassenraum bleibt 6/6/6/6. Das Leitfaden-Panel ist Text und immer sichtbar, solange ein Auftrag lĂ€uft — kein neuer Toggle.

Layout. Inspektorbreite per Pointer-Drag auf der Trennlinie, persistiert in einst.layout.inspBreite (Grenzen 280–560, Default 360, Doppelklick = Default); Tastatur ĂŒber Alt+←/→. Der Griff ist role="separator" (kein [role=button], kein input) und damit kein R12-Bedienelement; er bleibt per Tastatur bedienbar.

QualitĂ€t. Neue Gates: tests/leitfaden*.test.js (Erkennung, PrĂŒfungsmodus, Determinismus), tests/onboarding-tour.test.js, tests/labor-aktionen.test.js, tests/layout-breite.test.js, tests/kein-leck.test.js (keine Lösungen in Texten/ZustĂ€nden vor der Aktion). Alle neuen Tests sind auto-Teil der Suite; R12/44-px-Grenzen und der Detektor bleiben unverĂ€ndert.

Nicht gemessen (3.1.0, ehrlich). R12-Neumessung und Sicht erst im 3.1-Gate; APK/SHA256SUMS bleiben ohne Android-Keystore-Secrets rot (BAU-02). GerÀte-/Screenreader-Test weiterhin offen.