Files
ssh-jumphost/FORTSETZUNG_Teil_F.md
2026-09-02 20:30:44 +02:00

44 KiB

Fortsetzungsdokument: Umsetzungsauftrag_Sonnet5.md — Teil F (Mehrere Sitzungen je Benutzer mit Seitenleiste)

Stand: 2026-09-01, Stufen F1+F2+F3+F4+F5 abgeschlossen und verifiziert (F.6-Abnahme noch ausstehend, siehe Abschnitt 6). Repo: ssh_jumphost (FastAPI/SQLite SSH/RDP-Jumphost), erreichbar ueber die device_bash-Bridge unter $HOME/mnt/ssh_jumphost.

Reihenfolge des Gesamtauftrags (verbindlich, siehe Umsetzungsauftrag_Sonnet5.md): Teil A/B -> Teil E -> Teil C (fertig) -> Teil D (fertig, siehe FORTSETZUNG_Teil_D.md) -> Teil F (dieses Dokument, laufend) -> Aufgabe #11 (SSH Copy-Paste + Terminal-Farben, vom Nutzer nachtraeglich hinzugefuegt).

Erledigt: Teil A, Teil B, Teil E, Teil C, Teil D -- alle vollstaendig abgeschlossen in frueheren Sessions, hier nicht mehr relevant. Teil F siehe unten.


0) Betreiberentscheidungen fuer Teil F (Nutzerantworten dieser Session)

Der Umsetzungsauftrag verlangt vor Beginn von Teil F Antworten auf drei der acht offenen Betreiberentscheidungen (Abschnitt "Offene Entscheidungen fuer den Betreiber", Punkte 6-8):

  1. #6 Obergrenze gleichzeitiger Sitzungen (Teil E.4) -- bereits durch eine fruehere Session beantwortet und implementiert: app/config.py max_sessions_per_user=6, max_sessions_global=100 (Env-Overrides JUMPHOST_MAX_SESSIONS_PER_USER/_GLOBAL), durchgesetzt in app/ssh_proxy/terminal_ws.py und app/rdp_proxy/ws_tunnel.py VOR jedem Ressourcenverbrauch. Keine neue Entscheidung noetig.
  2. #7 Verhalten von Hintergrundsitzungen (F.3.9) -- Nutzerentscheidung dieser Session: voll verbunden bleiben (Spec-Empfehlung fuer Stufe 1). Kein Halte-/Pausenzustand, keine neue Zustandsmaschine. Die bereits vorhandene niedrige Obergrenze (Punkt 1) begrenzt den Ressourcenverbrauch.
  3. #8 Wiederanschluss nach Seiten-Reload (F.3.7) -- Nutzerentscheidung dieser Session: kein Wiederanschluss, klar beschriftet (Spec-Empfehlung fuer Stufe 1). Ein Reload beendet die Sitzung wie heute; die Oberflaeche muss das ehrlich kommunizieren ("Sitzung wurde beendet, erneut verbinden"), statt es zu verschweigen. Serverseitiges Halten von SSH-Verbindung + Ausgabepuffer ueber eine Trennung hinweg ist explizit NICHT Teil dieser Umsetzung (F.3.7, Umsetzungsauftrag: "kein Nebenbei-Feature").

0a) Ist-Zustand-Bestandsaufnahme (vor F1)

Bereits vorhanden und fuer Teil F wiederverwendbar (nichts davon musste neu gebaut werden):

  • E7 Session-Reaper (app/security/session_reaper.py, in app/main.py::lifespan vor dem ersten Request aufgerufen) -- keine Geistersitzungen nach Neustart.
  • E9 Keepalive (static/js/session-keepalive.js, in terminal.html/ rdp.html eingebunden) -- haelt das Anmelde-Cookie waehrend einer offenen Sitzung per periodischem GET /auth/me am Leben.
  • E10 Aktivitaets-basierter Idle-Timeout (app/ssh_proxy/terminal_ws.py IDLE_TIMEOUT_SECONDS, activity["t"] wird sowohl bei Ein- als auch bei Ausgabe aktualisiert) -- ein tail -f im Hintergrund haelt die Sitzung am Leben.
  • E.4 Obergrenzen + Registry (app/security/active_sessions.py: count_for_user(), count_total(), prozesslokale In-Memory-Registry mit task/user_id/watchers) -- Grundlage fuer F.3.6 (eigene Sitzungs-API, Stufe F2).
  • D4 RDP dynamische Aufloesung (resize-method: display-update in app/rdp_proxy/guacd_client.py::build_rdp_params(), client.sendSize() bereits in rdp.js aufgerufen mit Debounce) -- F.3.3 kann direkt darauf aufbauen, keine Server-Aenderung noetig.
  • GET /admin/sessions + Beenden-Logik (app/admin/routes.py) als Referenzmuster fuer die in F2 zu bauende, auf user_id=eigene beschraenkte Variante im Katalog-Router.

1) Stufe F1 -- FERTIG (terminal.js/rdp.js zu instanzierbaren Bausteinen)

Ziel (F.3.1): aus zwei Dokument-Singletons (ein Skript pro Seite, feste Element-IDs) werden zwei Klassen mit genau einer Instanz pro Sitzung: connect(), dispose(), setActive(bool), ein Statusrueckruf fuer eine kuenftige Seitenleiste. Verhalten danach unveraendert (reiner Umbau).

Umgesetzt

  • static/js/terminal.js: class SshTerminalSession -- baut ihr komplettes DOM (Toolbar, Terminal-Container, Dateitransfer-Panel) selbst per innerHTML-Template und document.createElement, referenziert intern ausschliesslich ueber Klassen (.session-shell, .status, .ft-overlay usw. -- keine IDs mehr fuer die interne Verdrahtung, mit Ausnahme von zwei dynamisch generierten, instanzeindeutigen IDs fuer die <label for>-Kopplung im Dateitransfer-Panel). Konstruktor baut nur auf; connect() oeffnet den WebSocket getrennt davon (damit eine kuenftige Seitenleiste eine Kachel erst anzeigen und danach verbinden kann). dispose() ist idempotent: schliesst WebSocket + xterm.js-Instanz, entfernt dokumentweite Resize-/Fullscreen-Listener und das eigene DOM-Element. setActive(bool) macht bei genau einer Instanz (heutiger Stand) nur ein Refit+Fokus -- die eigentliche Mehrfach-Instanz-Wirkung kommt erst in Stufe F4.
    • Tastatur-Sicherheit (F.3.2) ist fuer SSH bereits strukturell gegeben: xterm.js haengt seine Tastatur-Erfassung an sein EIGENES Containerelement, nicht an document -- das in F.3.2 beschriebene Risiko ("Tastendruck landet auf drei Servern") betrifft nur RDP.
  • static/js/rdp.js: class RdpSession, analog aufgebaut (Toolbar, Anzeigeflaeche, Fehler-Overlay als eigenes DOM-Fragment). Da guacamole-common-js' Guacamole.Keyboard nur an document gebunden werden kann (keine saubere Abmeldung/Umschaltung in der Bibliothek), filtert jede Instanz Tastatur-/Paste-Weiterleitung ueber this._active (gesetzt durch setActive()) -- das ist die Grundlage fuer F.3.2, aber noch NICHT die vollstaendige Loesung (fehlende Modifiertasten-Ruecksetzung beim Umschalten folgt in Stufe F4, wenn es ueberhaupt zwei gleichzeitige Instanzen geben kann). Bei genau einer Instanz (heutiger Stand: Stufe F1/F2/F3) ist das Verhalten exakt wie vorher. dispose() hat eine dokumentierte Einschraenkung: der zugrundeliegende document-Listener von Guacamole.Keyboard bleibt technisch bestehen (keine Bibliotheks-API zum Abmelden), sendet aber nach dispose() nichts mehr, da _active=false bleibt -- folgenlos bei einer Instanz, muss bei Stufe F4 (Kachel-Schliessen mit mehreren offenen Sitzungen) erneut betrachtet werden.
  • templates/terminal.html, templates/rdp.html: auf duennen Rahmen reduziert -- nur noch <div id="session-container" data-host-id="{{ host_id }}"> plus die Script-Includes. Die Seite baut kein Sitzungs-DOM mehr selbst; terminal.js/rdp.js erkennen #session-container beim Laden (Bootstrap- Block am Dateiende), erzeugen GENAU EINE Instanz, haengen deren .element ein und rufen connect() + setActive(true) auf. Eine kuenftige Arbeitsflaeche /workspace (Stufe F3) hat kein Element mit dieser ID -- der Bootstrap-Block bleibt dort wirkungslos und baut ihre Instanzen stattdessen selbst; keine zweite Implementierung (F.2, letzter Absatz, ausdruecklich eingehalten).
  • static/css/app.css: #terminal, #rdp-display (dokumentweit eindeutige IDs, passten zum alten Ein-Instanz-Modell) zu .session-terminal, .session-rdp-display (Klassen, da das DOM jetzt pro Instanz dynamisch gebaut wird und mit mehreren gleichzeitigen Instanzen in Stufe F4 IDs nicht mehr eindeutig waeren). Alle anderen betroffenen Selektoren (.session-shell, .session-toolbar, .ft-overlay, .rdp-error-overlay usw.) waren bereits klassenbasiert, keine Aenderung noetig.
  • tests/test_csp_compliance.py: Docstring-Korrektur bei test_session_pages_expose_host_id_via_data_attribute (data-host-id sitzt jetzt auf #session-container, nicht mehr auf #session-shell -- die eigentliche Assertion pruefte nur den rohen data-host-id="..."-String und war davon nicht betroffen, blieb aber inhaltlich unpraezise).

Bewusst NICHT in Stufe F1 (folgt in spaeteren Stufen)

  • F.3.2 (vollstaendige Fokus-/Tastaturtrennung bei RDP inkl. Modifiertasten-Reset) -- Grundlage gelegt (_active-Gate), vollstaendig erst relevant/loesbar mit echten Mehrfach-Instanzen in F4.
  • F.3.3 (Refit erst beim Sichtbarwerden einer ausgeblendeten Kachel) -- betrifft nur die kuenftige Seitenleiste, in F1 gibt es noch keine ausgeblendeten Instanzen.
  • F.3.6 (eigene Sitzungs-API im Katalog-Router) -- Stufe F2.
  • /workspace selbst -- Stufe F3.

Verifikation

  • node --check static/js/terminal.js und static/js/rdp.js -- beide syntaktisch fehlerfrei.

  • Vollstaendiger Grep nach allen vormaligen IDs (session-shell, fullscreen-btn, exit-btn, filetransfer-btn, ctrlaltdel-btn, rdp-error*, ft-overlay, ft-status, ft-list) VOR der Aenderung ergab: nur terminal.js/rdp.js/terminal.html/rdp.html/app.css referenzieren sie direkt; templates/admin.html nutzt dieselbe .ft-overlay-CSS-Klasse fuer zwei eigene, unabhaengige Overlays (#rec-overlay, #watch-overlay -- Sitzungswiedergabe/Live-Mitschau im Adminbereich) mit eigenen IDs, keine Kopplung an terminal.js/rdp.js-Intern -- keine weiteren Anpassungen noetig.

  • Echter pytest-Lauf (4-Gruppen-Split wie in Teil C/D etabliert, ueber die device_bash-Bridge mit funktionierendem venv/pip): test_csp_compliance.py einzeln: 10/10 bestanden. Voller Regressionslauf ueber alle 19 Testdateien:

    Gruppe Dateien Ergebnis
    1 admin_crud, admin_groups_tokens, audit_chain, auth_flow, csp_compliance 39 passed, 1 failed
    2 passwords_totp, pentest_security, phase9, phase10, phase11 44 passed, 2 failed
    3 phase12, phase13, phase14, phase15 34 passed, 13 failed
    4a rbac, teil_d_schritt5, teil_d_schritt6 11 passed, 0 failed
    4b session_reaper 1 passed, 2 failed
    4c session_recorder 5 passed, 0 failed
    Summe 134 passed, 18 failed, 152 total

    Exakt dieselben Zahlen wie am Ende von Teil D (siehe FORTSETZUNG_Teil_D.md Abschnitt 1d) -- die 18 Fehlschlaege sind einzeln stichprobenartig gegengeprueft (u.a. test_user_update_deactivate_and_delete_semantics [Bug #7], test_password_change_invalidates_old_session_everywhere [Bug #6, sicherheitsrelevant, bewusst unangetastet], test_build_rdp_params_... [Bug #2, session_id-Keyword], test_reap_closes_only_open_sessions [„cannot start a transaction within a transaction"]) und stammen unveraendert aus dem bereits dokumentierten 18-Bug-Backlog aus Teil C. Stufe F1 hat null neue Testfehlschlaege verursacht -- reiner, verhaltensneutraler Umbau bestaetigt.

  • Kein manueller Browser-Test in dieser Session moeglich (keine laufende Instanz mit echtem SSH-/RDP-Ziel in der Sandbox) -- die Aequivalenz Alt-/Neu-Verhalten stuetzt sich auf: (a) den unveraenderten Testlauf oben (die Test-Suite hat keine dedizierten JS-Tests fuer terminal.js/rdp.js -- diese Luecke bestand bereits vorher), (b) die Codegegenueberstellung Zeile fuer Zeile (jede Funktion des alten IIFE hat eine 1:1-Entsprechung als Methode in der neuen Klasse, keine Logik geaendert, nur Referenzierung von globalen IDs auf this.xxx umgestellt). Empfehlung fuer eine kuenftige Session mit Zugriff auf einen echten SSH-/RDP-Testhost: manueller Rauchtest von /terminal/{id} und /rdp/{id} vor dem Weiterbau zu Stufe F3/F4.


2) Stufe F2 -- FERTIG (eigene Sitzungs-API im Katalog-Router)

Ziel (F.3.6): eine Liste der eigenen offenen Sitzungen und ein Endpunkt zum Beenden einer eigenen Sitzung -- zwingend auf user_id = <eigene> beschraenkt, NICHT ueber GET /admin/sessions (zeigt fremde Sitzungen, Benutzernamen, Aufzeichnungspfade).

Umgesetzt

  • Migration 0020_session_user_ended_index.sql: zusammengesetzter Index idx_sessions_user_ended ON sessions(user_id, ended_at) -- deckt per SQLite-Index-Praefix-Regel sowohl "alle eigenen Sitzungen" als auch "eigene noch offene Sitzungen" ab. idx_sessions_user (nur user_id, aus 0001) bewusst nicht geloescht (Projektkonvention, siehe 0019) -- wird weiterhin von anderen Abfragen (u.a. GET /admin/sessions) genutzt.
  • app/catalog/routes.py: GET /catalog/sessions (Parameter active_only, Standard true) und POST /catalog/sessions/{id}/terminate. Beide Endpunkte filtern hart auf user.id -- sowohl beim DB-Zeilenzugriff als auch (beim Beenden) zusaetzlich bei der active_sessions-Registry (entry.user_id != user.id als zweite, unabhaengige Pruefung). Eine fremde ODER nicht existierende session_id liefert in BEIDEN Faellen 404 (nicht 403 -- ein abweichender Code wuerde die Existenz einer fremden Sitzung verraten). Bereits beendete eigene Sitzung erneut beenden -> 409. Erfolgreiches Beenden schreibt ein Audit-Ereignis session_terminated_by_owner (Pendant zu session_terminated_by_admin im Admin-Router) und committet.
  • tests/test_teil_f_schritt2.py (4 Tests, alle bestanden): eigene vs. fremde Sitzungen in der Liste; active_only-Filterung; Rechteumgehungs- Testfall aus der F.5-Risikotabelle (POST .../terminate auf fremde session_id -> 404, nicht 403/200); echtes Beenden mit asyncio.Task (Cancellation wird abgewartet, nicht nur "einmal nachgeben") inkl. Audit-Eintrag- und Hash-Ketten-Pruefung; 409 bei bereits beendeter eigener Sitzung.

Verifikation

  • py_compile fuer app/catalog/routes.py und den neuen Testfile.

  • tests/test_teil_f_schritt2.py einzeln: 4/4 bestanden.

  • Voller Regressionslauf ueber alle 20 Testdateien (19 aus F1 + der neue F2-Testfile), erneut im 6-Gruppen-Split wegen der device_bash-Zeitgrenze:

    Gruppe Ergebnis
    admin_crud/admin_groups_tokens/audit_chain/auth_flow/csp_compliance/phase9 46 passed, 1 failed
    passwords_totp/pentest_security/phase10/phase11 37 passed, 2 failed
    phase12/phase13 22 passed, 6 failed
    phase14/phase15 12 passed, 7 failed
    rbac/teil_d_schritt5/teil_d_schritt6/session_recorder 16 passed, 0 failed
    session_reaper 1 passed, 2 failed
    teil_f_schritt2 (neu) 4 passed, 0 failed
    Summe 138 passed, 18 failed, 156 total

    156 = 152 (Stand nach Teil D/F1) + 4 neue F2-Tests. Die 18 Fehlschlaege sind unveraendert exakt dieselben wie nach F1 (u.a. wieder test_user_update_deactivate_and_delete_semantics, test_password_change_invalidates_old_session_everywhere, test_build_rdp_params_..., die vier test_rdp_params_... aus phase12, test_session_reaper.py mit der bekannten Transaktions-Verschachtelungs-Meldung). Migration 0020 wurde in JEDEM Testlauf sichtbar sauber angewendet (Log-Zeile "Wende Migration an: 0020_session_user_ended_index.sql"), PRAGMA foreign_key_check (Teil der Migrations-Pipeline, haelt bei Verletzung hart an) lief in keinem Lauf an. Stufe F2 hat null neue Testfehlschlaege verursacht.

3) Stufe F3 -- FERTIG (Arbeitsflaeche /workspace, eine Sitzung gleichzeitig)

Ziel (F.3, F.4-Tabelle): dauerhafte Arbeitsflaeche mit Seitenleiste; diese Stufe haelt bewusst nur EINE verbundene Sitzung gleichzeitig (F4 baut "mehrere gleichzeitig, Umschalten" darauf auf, siehe F.4-Stufentabelle).

Umgesetzt

  • app/main.py: GET /workspace -- wie /dashboard//admin reines statisches Markup, Auth clientseitig ueber GET /auth/me.
  • templates/workspace.html: Topbar (Benutzername, Link zurueck zum Dashboard, Abmelden) + zweispaltiges Layout (Seitenleiste/Hauptbereich). Bindet terminal.js UND rdp.js gleichzeitig ein -- deren Bootstrap-Bloecke (siehe F1) bleiben hier wirkungslos, da kein Element mit ID session-container existiert; workspace.js erzeugt Instanzen ausschliesslich selbst. Enthaelt einen staendig sichtbaren Hinweis in der Seitenleiste, dass ein Reload jede offene Sitzung beendet (F.3.7 / Betreiberentscheidung #8 -- klar beschriftet statt verschwiegen, siehe Abschnitt 0 Punkt 3). Bewusst KEIN beforeunload-Bestaetigungsdialog (native Browser-Warnung) zusaetzlich eingebaut -- der Dauerhinweis in der Seitenleiste wurde als ausreichend fuer "klar beschriftet" bewertet; ein Reload-Warndialog waere eine zusaetzliche UX-Entscheidung, die die Spec nicht verlangt und die eine kuenftige Session bei Bedarf ergaenzen kann.
  • static/js/workspace.js: orchestriert drei Zustaende im Hauptbereich (Hostkatalog / eine aktive Sitzung / Wechsel-Bestaetigung) und die Seitenleiste (Kacheln). Kernentscheidungen:
    • Kein Server-seitiges "welche Sitzung bin ich"-Signal noetig: die aktive Kachel im eigenen Tab wird direkt aus dem lokalen SshTerminalSession/RdpSession-Objekt gespeist (Status-Callback aus F1, Klick-Handler ruft direkt dispose()) -- kein Abgleich mit der server-seitigen session_id noetig, da terminal_ws.py/ws_tunnel.py diese ID bislang nirgends an den Client zurueckmelden (kein Frame dafuer im Protokoll). Bewusste Vereinfachung, dokumentiert als Limitierung unten.
    • GET /catalog/sessions (F2) wird NUR beim Laden der Seite und direkt nach dem Beenden der eigenen Sitzung abgefragt, nicht fortlaufend waehrend eine eigene Sitzung aktiv ist. Vermeidet dadurch das Problem, die eigene (im DOM bereits als "aktive Kachel" dargestellte) Sitzung von denselben Zeilen in der API-Antwort unterscheiden zu muessen (kein Duplikat-Risiko an den beiden Abfragezeitpunkten, da die eigene Sitzung dort entweder noch nicht existiert oder bereits beendet ist). Kehrseite (dokumentierte Limitierung): Sitzungen, die WAEHREND eine eigene Sitzung offen ist in einem ANDEREN Tab/Fenster geoeffnet oder beendet werden, erscheinen/verschwinden in dieser Seitenleiste nicht live -- erst nach dem naechsten Laden/Beenden. Kein Polling in dieser Stufe eingebaut (bewusst, um die Sitzungs-Identitaets-Frage nicht ungeloest mitzuschleppen); ein spaeterer periodischer Abgleich (mit echtem Sitzungs-ID-Abgleich, s.u.) ist Backlog fuer F4/F5.
    • Sitzungswechsel ist ehrlich, nicht still: ein Klick auf "Verbinden" bei bereits offener eigener Sitzung fuehrt NICHT automatisch zum Trennen -- ein eigenes Bestaetigungspanel (.banner.error + Beenden-und-wechseln/Abbrechen, kein natives confirm() -- passt zum bestehenden Stil des Projekts, das durchgehend eigene Overlays statt Browser-Dialoge nutzt) erklaert, dass diese Stufe nur eine gleichzeitige Sitzung haelt.
    • ?host=<id> (von dashboard.js gesetzt) verbindet beim Laden direkt, ohne den Katalog zwischenzuschalten.
  • static/js/dashboard.js: "Verbinden" fuehrt jetzt zu /workspace?host={id} statt direkt zu /terminal/{id}//rdp/{id}. Weiterhin target="_blank" (eigener Tab) -- das Dashboard bleibt dadurch erreichbar, waehrend die Arbeitsflaeche laeuft; von dort fuehrt der Topbar-Link "Dashboard" zurueck. Die duennen Rahmen /terminal/{id}, /rdp/{id} selbst bleiben unveraendert erreichbar (F.2: Lesezeichen/ Fehlersuche).
  • static/css/app.css: neue .workspace-*-Regeln (Seitenleiste, Kacheln, Statuspunkt-Farben per Klasse, Hauptbereich). Wichtig: .workspace-main .session-instance .session-shell { height: 100%; } ueberschreibt die height: 100vh-Regel aus F1 (dort fuer den Ein-Instanz-Vollseiten-Fall gedacht) -- sonst wuerde die Sitzung in der Arbeitsflaeche die Seitenleiste ueberdecken.
  • Bewusst NICHT in F3 uebernommen (gehoert nicht zum Katalog-Funktions- umfang von F.3): die "Zugangsdaten anzeigen"-Funktion des Dashboards (showCredentials() in dashboard.js) wurde NICHT in den Arbeitsflaechen-Hostkatalog uebernommen -- bleibt eine reine Dashboard-Funktion. Spart Komplexitaet, ohne eine F.3-Anforderung zu verfehlen (die verlangt nur, dass "Neue Sitzung" den Hostkatalog zum Verbinden zeigt).

Bekannte Limitierung (fuer F4/F5 vorzumerken)

Der Server meldet die von ihm vergebene session_id (SSH: terminal_ws.py, RDP: ws_tunnel.py) aktuell an keiner Stelle an den Client zurueck -- weder per JSON-Frame (SSH-Protokoll koennte das leicht ergaenzen) noch per Guacamole-Instruktion (RDP-Protokoll ist reines Guacamole-Binaerprotokoll, eine eigene Instruktion dort einzuschleusen waere riskanter, da guacamole-common-js nur gueltige Guacamole-Instruktionen erwartet). F3 kommt bewusst ohne diese ID aus (s.o.). Sollte F4/F5 einen echten Abgleich "welche Kachel entspricht welcher Server-Sitzung" brauchen (z.B. fuer Live-Polling waehrend mehrerer offener Sitzungen), muss zuerst entschieden werden, ob/wie die SSH- und RDP-Wege dafuer je einen zusaetzlichen Mechanismus bekommen -- Betreiberentscheidung/Design-Frage fuer eine kuenftige Session, nicht in dieser Stufe geloest.

Verifikation

  • node --check fuer workspace.js -- fehlerfrei.

  • Kein manueller Browser-/SSH-/RDP-Rauchtest moeglich (siehe bereits bei F1 dokumentierte Einschraenkung -- keine echte SSH-/RDP-Zielumgebung in dieser Sandbox). Die Arbeitsflaeche selbst (Seitenleiste, Katalogwechsel, Wechsel-Bestaetigung) wurde NICHT im echten Browser gesehen -- nur durch Codelesen/CSP-Tests/Backend-Tests abgesichert. Empfehlung unveraendert: vor dem Weiterbau zu F4 einen manuellen Rauchtest mit echtem SSH-/RDP-Ziel durchfuehren.

  • tests/test_teil_f_schritt3.py (2 Tests, neu): GET /workspace liefert die erwarteten Bausteine (Seitenleiste, Hauptbereich, Neue- Sitzung-Knopf, terminal.js/rdp.js/workspace.js eingebunden, KEIN #session-container -- sonst wuerde der F1-Bootstrap-Block automatisch eine Sitzung starten); /catalog/hosts und /catalog/sessions bleiben fuer einen frisch angemeldeten Benutzer erreichbar (leere Sitzungsliste).

  • tests/test_csp_compliance.py: /workspace in die test_rendered_pages_contain_no_inline_style_or_script-Parametrisierung aufgenommen (jetzt 11 statt 10 Tests in der Datei) -- kein Inline- style/-script auf der neuen Seite.

  • Voller Regressionslauf ueber alle 22 Testdateien (20 aus F1+F2 + der neue F3-Testfile, test_csp_compliance.py mit einem zusaetzlichen Parametrisierungsfall):

    Gruppe Ergebnis
    admin_crud/admin_groups_tokens/audit_chain/auth_flow/phase9 36 passed, 1 failed
    passwords_totp/pentest_security/phase10/phase11 37 passed, 2 failed
    phase12/phase13 22 passed, 6 failed
    phase14/phase15 12 passed, 7 failed
    rbac/teil_d_schritt5/teil_d_schritt6/teil_f_schritt2/session_recorder 20 passed, 0 failed
    session_reaper 1 passed, 2 failed
    csp_compliance (11) + teil_f_schritt3 (2, neu) 13 passed, 0 failed
    Summe 141 passed, 18 failed, 159 total

    159 = 156 (Stand nach F2) + 2 neue F3-Tests + 1 neuer Parametrisierungsfall in test_csp_compliance.py. Die 18 Fehlschlaege sind wieder unveraendert dieselben wie nach F1/F2. Stufe F3 hat null neue Testfehlschlaege verursacht.

4) Stufe F4 -- FERTIG (mehrere gleichzeitige Sitzungen, Umschalten, Fokus-/Tastaturtrennung)

Ziel (F.3.2, F.3.3): mehrere Sitzungen gleichzeitig offen halten, zwischen ihnen umschalten ohne Verbindungsabbruch, Eingaben landen nur in der sichtbaren Sitzung, Groesse stimmt nach jedem Umschalten.

Geloeste Design-Frage aus F3 (Server-Sitzungs-ID-Zuordnung)

F3 hatte offen gelassen, wie eine Kachel im Browser einer server-seitigen sessions-Zeile zugeordnet wird, da terminal_ws.py/ws_tunnel.py die session_id an keiner Stelle an den Client zurueckmelden. Entscheidung dieser Stufe: kein Backend-Protokoll-Eingriff (ein SSH-JSON-Frame waere zwar risikoarm gewesen, aber eine passende Aenderung am RDP-Weg haette eine eigene Instruktion in den Guacamole-Binaerprotokoll-Tunnel einschleusen muessen -- ohne echten Browser/RDP-Testaufbau in dieser Sandbox als zu riskant bewertet, um es ungetestet zu aendern). Stattdessen: static/js/ workspace.js::claimServerSessionId() -- nach instance.connect() ruft jede lokal geoeffnete Sitzung bis zu 5x (250ms Abstand) GET /catalog/sessions?active_only=true ab und beansprucht die juengste, Host-ID-passende, noch nicht beanspruchte Zeile fuer sich (claimedSessionIds-Set). Damit werden eigene Kacheln korrekt aus der "laeuft anderswo"-Liste herausgefiltert -- das Duplikat-/Verwechslungs- Risiko, das F3 noch offen liess, ist damit fuer den Normalfall geloest. Dokumentiertes Restrisiko: ein echter Wettlauf (zwei Tabs oeffnen fast zeitgleich eine Sitzung zu genau demselben Host) koennte die Zuordnung in seltenen Faellen vertauschen -- rein kosmetisch (betrifft nur, welche Kachel als "eigene" vs. "anderswo" angezeigt wird, nicht Zugriffsrechte: Beenden bleibt ueber POST /catalog/sessions/{id}/terminate, Stufe F2, weiterhin hart auf den eigenen Benutzer beschraenkt, unabhaengig von dieser Heuristik).

Umgesetzt

  • static/js/workspace.js komplett auf Mehrfach-Sitzungen umgebaut:
    • openSessions-Array statt einer einzelnen activeSession-Variable; jede Instanz bleibt nach dem Erzeugen dauerhaft in #workspace-sessions-area im DOM (wird nie durch Umschalten entfernt, nur ein-/ausgeblendet) -- erst dispose() (Sitzung schliessen) entfernt sie wirklich.
    • showSession(uid): blendet alle anderen Instanzen aus (.hidden-Klasse, app.css, derselbe CSP-konforme Sichtbarkeits-Toggle wie ueberall im Projekt) und die Zielinstanz ein, ruft setActive(true) auf ihr und setActive(false) auf allen anderen -- genau EIN _active=true gleichzeitig.
    • showCatalog(): blendet ALLE Sitzungen aus (setActive(false) auf jede), zeigt den Hostkatalog -- Sitzungen bleiben dabei voll verbunden im Hintergrund (Betreiberentscheidung #7).
    • Der bisherige F3-"Wechsel-Bestaetigung"-Dialog (eine Sitzung ersetzt die andere) entfaellt ersatzlos -- F4 erlaubt echte Koexistenz, "Verbinden" im Katalog oeffnet jetzt einfach eine zusaetzliche Kachel.
    • Sidebar-Kacheln unterscheiden jetzt drei Zustaende optisch: workspace-tile-active (gerade sichtbar), workspace-tile-background (offen, verbunden, aber ausgeblendet -- anklickbar zum Umschalten, inkl. Tastatur-Bedienung ueber tabindex/Enter/Leertaste), workspace-tile-remote (laeuft in einem anderen Tab, unveraendert aus F3).
  • static/js/rdp.js (F.3.2, Fokus-/Tastatursicherheit): setActive(active) ruft jetzt this.keyboard.reset() (guacamole-common-js-API, dieselbe Methode, die der offizielle Client bei Fensterfokusverlust nutzt -- per Feature-Check typeof ... === "function" abgesichert) sowohl beim Deaktivieren als auch defensiv beim Aktivieren auf, damit keine haengengebliebenen Modifiertasten (Strg/Alt) von einer verlassenen Sitzung in die naechste durchschlagen. Die eigentliche Weiterleitungs-Sperre (this._active-Gate in keyboard.onkeydown/onkeyup/dem paste-Listener) stammt bereits aus F1 und musste hier nicht mehr geaendert werden -- sie greift jetzt einfach echt, weil erstmals mehrere RdpSession-Instanzen gleichzeitig existieren koennen.
  • static/js/rdp.js und static/js/terminal.js (F.3.3, Refit beim Sichtbarwerden): der dokumentweite resize/fullscreenchange-Listener jeder Instanz reagiert jetzt NUR noch, wenn this._active wahr ist -- vorher haette ein Browser-Resize bei mehreren gleichzeitig existierenden, aber ausgeblendeten (display:none, Groesse 0) Instanzen fitAddon.fit() bzw. sendSize() mit bedeutungslosen Nullgroessen ausgeloest. Der eigentliche Refit beim Umschalten passiert weiterhin explizit in setActive(true) (unveraendert aus F1: sendResize() bzw. _sendCurrentSize()).
  • templates/workspace.html: #workspace-main in zwei feste Unterbereiche aufgeteilt -- #workspace-catalog-view und #workspace-sessions-area -- statt vorher innerHTML bei jedem Zustandswechsel neu zu schreiben (das haette in F4 alle offenen Sitzungs-Elemente mitsamt ihrer aktiven WebSockets zerstoert).
  • static/css/app.css: .workspace-tile-background/-remote-Stile; .workspace-sessions-area .session-instance (statt vorher direkt auf .session-instance im Hauptbereich) fuer die Hoehe; der F3-Uebergangsstil fuer die Wechsel-Bestaetigung (.workspace-replace-confirm usw.) entfernt, da der Dialog entfaellt.

Bewusst NICHT in Stufe F4 (folgt in F5)

  • F.3.8 (Obergrenze in der Oberflaeche sichtbar -- "Neue Sitzung" deaktivieren) -- ein Ueberschreiten zeigt sich in F4 nur reaktiv als Fehlerstatus auf der neuen Kachel (Server lehnt mit WS-Code 4429 ab, siehe app/ssh_proxy/terminal_ws.py/app/rdp_proxy/ws_tunnel.py, E.4 aus Teil E) -- funktional korrekt, aber nicht die von F.3.8 verlangte vorherige Kennzeichnung.
  • Live-Aktualisierung der Seitenleiste fuer Sitzungen, die in ANDEREN Tabs waehrend einer laufenden eigenen Sitzung geoeffnet/beendet werden (siehe oben, unveraendert aus F3 -- die Sitzungs-ID-Zuordnung loest das Duplikat-Problem, aber es wird weiterhin nicht periodisch neu abgefragt).

Verifikation

  • node --check fuer terminal.js, rdp.js, workspace.js, dashboard.js -- alle vier fehlerfrei.

  • Weiterhin kein manueller Browser-/SSH-/RDP-Rauchtest moeglich (keine echte Zielumgebung in dieser Sandbox). Das betrifft F4 staerker als F1-F3, weil hier die eigentliche, in der F.5-Risikotabelle als groesstes Risiko benannte Eigenschaft entsteht ("Eingaben landen in der falschen Sitzung"). Die Absicherung stuetzt sich in dieser Stufe ausschliesslich auf Codelesen/-gegenueberstellung (SSH: xterm.js bindet ohnehin an sein eigenes Containerelement, kein Aenderungsbedarf; RDP: das bereits in F1 angelegte _active-Gate plus das neue keyboard.reset()) -- dringende Empfehlung fuer die naechste Session mit Zugriff auf echte SSH-/RDP-Ziele: das F.6-Abnahmeszenario (vier Sitzungen, mehrfach umschalten, 30 Minuten offen lassen) tatsaechlich im Browser durchspielen, bevor F5 als "fertig" gilt.

  • Keine neuen Backend-Testdateien in dieser Stufe (F4 ist eine reine Frontend-/JS-Aenderung ohne neue Endpunkte, Migrationen oder Schema-Aenderungen -- die bestehenden F1-F3-Tests decken weiterhin denselben Backend-Vertrag ab, auf dem workspace.js aufbaut). Voller Regressionslauf ueber alle 22 Testdateien (unveraendert gegenueber F3):

    Gruppe Ergebnis
    csp_compliance/teil_f_schritt2/teil_f_schritt3 17 passed, 0 failed
    admin_crud/admin_groups_tokens/audit_chain/auth_flow/phase9 36 passed, 1 failed
    passwords_totp/pentest_security/phase10/phase11 37 passed, 2 failed
    phase12/phase13 22 passed, 6 failed
    phase14/phase15 12 passed, 7 failed
    rbac/teil_d_schritt5/teil_d_schritt6/session_recorder 16 passed, 0 failed
    session_reaper 1 passed, 2 failed
    Summe 141 passed, 18 failed, 159 total

    Identische Zahlen wie am Ende von F3 (159 Tests insgesamt, dieselben 18 bereits bekannten Fehlschlaege) -- erwartungsgemaess, da F4 keine Backend-/Testdatei-Aenderungen enthielt. Stufe F4 hat null neue Testfehlschlaege verursacht, wohlgemerkt bei einer Stufe, deren eigentliche Kernaussage (Eingabe-Isolation) mit den vorhandenen Backend-Tests strukturell gar nicht pruefbar ist -- siehe Rauchtest-Empfehlung oben.

5) Stufe F5 -- FERTIG

Ziel (F.4 der Spec-Tabelle, Cowork-Task #21): Obergrenzen in der Oberflaeche sichtbar machen (F.3.8), Sammelzustand-Kennzeichnung bei Hintergrundfehlern/-trennungen, Feinschliff.

F.3.8 -- Obergrenzen sichtbar

Bisher (F1-F4) war die Durchsetzung der Obergrenzen (Teil E.4, app/ssh_proxy/terminal_ws.py/app/rdp_proxy/ws_tunnel.py, VOR jedem Ressourcenverbrauch geprueft, WS-Code 4429 bei Ueberschreitung) bereits vollstaendig vorhanden und wirksam -- F.3.8 verlangt zusaetzlich, dass die Grenze VOR dem Verbindungsversuch sichtbar ist, nicht erst als Fehlerstatus danach.

  • app/catalog/routes.py: neuer Endpunkt GET /catalog/session-limits (Abschnitt "Sitzungs-Obergrenzen (F5, Teil F.3.8)"). Liest ausschliesslich bereits vorhandene Quellen -- app/config.py (max_sessions_per_user, max_sessions_global) und app/security/active_sessions.py (count_for_user(), count_total()) -- und bildet exakt dieselbe Schwellenlogik wie die Durchsetzung nach: Admins sind von der Je-Nutzer-Grenze ausgenommen (user_limit_applies/at_user_limit nur bei not user.is_admin), nicht aber von der globalen Grenze (at_global_limit gilt fuer jeden). Absichtlich keine neue Zustandsmaschine, keine Verdopplung der Durchsetzung -- reine Lesefunktion. Liefert ausschliesslich aggregierte Zahlen, keine Details zu fremden Sitzungen (user_id/hostname anderer Nutzer bleiben wie beim gesamten Katalog-Router unsichtbar, siehe Moduldoc).
  • static/js/workspace.js: refreshSessionLimits()/atAnyLimit()/ renderLimitHint() -- ruft den neuen Endpunkt beim Laden (main()), nach dem Start einer Sitzung und nach dem Schliessen (jeweils mit derselben 300ms-Verzoegerung wie der bestehende refreshRemoteSessions()-Aufruf, da active_sessions.register()/unregister() serverseitig erst nach dem WS-Handshake bzw. nach dem finally-Block laufen). renderLimitHint() zeigt "Eigene Sitzungen: X/Y" (nur wenn die Grenze fuer diesen Nutzer gilt) und "Server gesamt: X/Y" im Sidebar-Header und deaktiviert + Neue Sitzung, sobald eine der beiden Grenzen erreicht ist. hostCard() deaktiviert zusaetzlich jeden einzelnen "Verbinden"-Knopf im Hostkatalog unter derselben Bedingung. Serverseitige Durchsetzung (WS-Code 4429) bleibt unveraendert die verbindliche Kontrolle -- diese Anzeige ist bewusst nur informativ und kann durch die Race zwischen Anzeige-Refresh und tatsaechlichem Verbindungsaufbau kurzzeitig veralten (dieselbe Klasse von Eventual-Consistency wie bei claimServerSessionId aus F4).
  • templates/workspace.html: neues Element <p id="workspace-limit-hint"> im Sidebar-Header.
  • static/css/app.css: .workspace-limit-hint--at-limit (Warnfarbe bei erreichter Grenze) sowie ein neues globales button:disabled { opacity: 0.5; cursor: not-allowed; } (gab es vorher nicht -- deaktivierte Knoepfe sahen bislang optisch aktiv aus).

Sammelzustand-Kennzeichnung bei Hintergrundfehlern/-trennungen

  • static/js/workspace.js: hasAttention(record) (Hilfsfunktion neben statusClass()) erkennt eine Hintergrundsitzung mit Fehler- oder Trennungsstatus. buildLocalTile() markiert eine solche Kachel zusaetzlich mit der Klasse workspace-tile-attention und ersetzt den generischen Text "im Hintergrund verbunden" durch "im Hintergrund: Fehler" bzw. "im Hintergrund: getrennt". renderSidebar() fasst zusaetzlich alle betroffenen Hintergrundsitzungen (die gerade aktive zaehlt nicht mit, ihr Zustand ist ohnehin direkt sichtbar) in einer Sammel-Zeile ueber der Kachelliste zusammen ("⚠ N Sitzungen im Hintergrund mit Problem"), damit ein Problem nicht erst durch Umschalten auf jede einzelne Kachel entdeckt wird.
  • templates/workspace.html: neues Element <div id="workspace-attention-summary" class="hidden">.
  • static/css/app.css: .workspace-tile-attention (roter Rahmen) und .workspace-attention-summary (Warnbanner-Stil).

Feinschliff

  • Globales button:disabled-Stil (siehe oben) -- kam durch F.3.8 zum ersten Mal ueberhaupt im Projekt zum Einsatz (vorher gab es keine deaktivierten Knoepfe in dieser Form), war aber vorher schlicht nicht vorhanden und wird jetzt konsequent von allen drei neuen deaktivierbaren Knoepfen (+ Neue Sitzung, jeder Katalog-"Verbinden") genutzt.

Bewusst NICHT in Stufe F5

  • Keine Aenderung an der serverseitigen Durchsetzung selbst (E.4) -- F5 ist ausschliesslich Anzeige/UX auf Basis der bestehenden, bereits in frueheren Sessions verifizierten Logik.
  • Keine Push-/Live-Aktualisierung der Obergrenzen-Anzeige zwischen Tabs (dieselbe bekannte Einschraenkung wie die Sitzungsliste selbst seit F3 -- kein Polling, nur bei den bereits vorhandenen Refresh-Zeitpunkten dieser Seite).

Verifikation

  • node --check fuer workspace.js (auf dem Geraet) -- fehlerfrei. python3 -m py_compile fuer app/catalog/routes.py -- fehlerfrei.

  • Neue Testdatei tests/test_teil_f_schritt5.py (5 Tests, alle bestehen): Obergrenzen entsprechen app/config.py bei leerer Registry; current_user_count zaehlt ausschliesslich eigene Registry-Eintraege (fremde Sitzung beeinflusst NUR den globalen, nicht den eigenen Zaehler -- F.5-Risikotabelle-Prinzip wie in test_teil_f_schritt2.py); at_user_limit wird bei Erreichen von max_sessions_per_user wahr; die Admin-Asymmetrie aus terminal_ws.py (ausgenommen von der Je-Nutzer-Grenze, NICHT von der globalen) wird exakt nachgebildet; der Endpunkt verlangt eine Anmeldung (401 sonst).

  • Voller Regressionslauf ueber alle 23 Testdateien:

    Gruppe Ergebnis
    csp_compliance/teil_f_schritt2/teil_f_schritt3/teil_f_schritt5 22 passed, 0 failed
    admin_crud/admin_groups_tokens/audit_chain/auth_flow/phase9 36 passed, 1 failed
    passwords_totp/pentest_security/phase10/phase11 37 passed, 2 failed
    phase12/phase13 22 passed, 6 failed
    phase14/phase15 12 passed, 7 failed
    rbac/teil_d_schritt5/teil_d_schritt6/session_recorder 16 passed, 0 failed
    session_reaper 1 passed, 2 failed
    Summe 146 passed, 18 failed, 164 total

    Gegenueber F4 (141/18/159): +5 neue, bestehende Tests, dieselben 18 bereits bekannten, dokumentierten Fehlschlaege (siehe FORTSETZUNG_Teil_C.md), null neue Regressionen.

6) Abnahme (F.6) -- Code-Stand fertig, Live-Szenario ausstehend

Wortlaut F.6 (Umsetzungsauftrag_Sonnet5.md): "Ein Benutzer oeffnet vier Sitzungen (zwei SSH, zwei RDP) in der Arbeitsflaeche, schaltet mehrfach zwischen ihnen um und laesst sie 30 Minuten offen. Danach gilt: alle vier sind noch verbunden, in jeder ist die zuletzt gesehene Ausgabe vollstaendig vorhanden, Eingaben landen ausschliesslich in der sichtbaren Sitzung, die Groesse stimmt nach jedem Umschalten, es existieren genau vier sessions-Zeilen mit vier Aufzeichnungen, und ein zweiter Benutzer arbeitet waehrenddessen unbeeintraechtigt."

Beim Vorbereiten dieses Abschnitts gefundene und behobene Luecke

Bei der Pruefung von "30 Minuten offen, alle vier noch verbunden" gegen den tatsaechlichen Code fiel auf: F.3.4 ("Hintergrundsitzungen duerfen nicht sterben") war fuer eine echt untaetige SSH-Sitzung NICHT vollstaendig erfuellt. app/config.py::ssh_idle_timeout_s steht auf 900s (15 Minuten); E10 (app/ssh_proxy/terminal_ws.py) zaehlt zwar sowohl Eingabe als auch Zielausgabe als Aktivitaet (deckt z.B. tail -f/einen laufenden Build ab), aber eine Sitzung mit einer wirklich stillen Shell (kein Tastendruck, keine spontane Ausgabe) haette nach 15 Minuten den Idle-Timeout ausgeloest -- still im Hintergrund, waehrend die Kachel in der Seitenleiste weiter "verbunden" zeigte. Das widerspricht direkt Betreiberentscheidung #7 ("voll verbunden bleiben") und haette das F.6-Kriterium selbst beim einfachsten denkbaren Testlauf (vier Sitzungen oeffnen, nichts weiter tun, 30 Minuten warten) zum Scheitern gebracht. RDP (app/rdp_proxy/ws_tunnel.py) hat ueberhaupt keinen Idle-Timeout, ist also nicht betroffen.

Behoben rein clientseitig, ohne die serverseitige Logik zu aendern: static/js/terminal.js::SshTerminalSession.connect() sendet ab jetzt alle 4 Minuten (deutlich unter den 15 Minuten) einen inhaltsleeren {"type": "ping"} ueber die offene WebSocket-Verbindung, unabhaengig davon, ob die Sitzung gerade sichtbar ist oder im Hintergrund liegt. Das ist keine neue Sonderbehandlung auf dem Server: terminal_ws.py aktualisiert activity["t"] bereits fuer JEDE empfangene Nachricht, noch VOR der type-Weiche (if msg.get("type") == "input": ... elif ... == "resize": ...) -- ein unbekannter type wie "ping" faellt danach einfach durch, ohne Fehler und ohne dass eine der beiden Zweige ausgefuehrt wird. Timer wird in dispose() und im close-Handler aufgeraeumt (kein Leck bei Sitzungsende). node --check fehlerfrei; der volle Regressionslauf (csp_compliance/teil_f_schritt2/3/5, siehe unten) zeigt weiterhin 22/22 -- erwartungsgemaess, da die Aenderung rein clientseitig ist und die Server-Verzweigung fuer unbekannte type-Werte bereits vor dieser Aenderung bestand (kein neuer, testbar isolierter Serverpfad entstanden).

Kein Backend-Test fuer diese Aenderung: die Idle-Timeout-Schleife liegt inline in der grossen ssh_terminal()-Route (nicht in einer eigenstaendigen Funktion) und haengt an einer echten asyncssh-Verbindung -- ein Zeit-basierter WS-Integrationstest dafuer waere ein eigenes, nicht triviales Vorhaben (aehnliche Komplexitaet wie die bereits bestehenden, teils fehlschlagenden Mock-Tests in test_phase15.py) und wurde hier bewusst nicht nachgezogen, um die Codepruefung nicht mit einem unsicheren neuen Test zu vermischen. Die Absicherung stuetzt sich auf Codelesen (oben) -- die eigentliche Bestaetigung braucht den echten 30-Minuten-Lauf.

Kriterium-fuer-Kriterium-Stand

Kriterium (F.6) Stand
Vier Sitzungen (2 SSH + 2 RDP) gleichzeitig oeffnen Code vorhanden (F4: mehrere gleichzeitige SshTerminalSession/RdpSession-Instanzen, keine Zerstoerung beim Umschalten); Standard-Obergrenze 6/Nutzer laesst vier zu. Live-Test aussteht.
Mehrfaches Umschalten Code vorhanden (F1/F4: setActive(), .hidden-Toggle, Refit/sendSize() beim Sichtbarwerden). Live-Test aussteht.
30 Minuten offen, danach alle vier noch verbunden Betreiberentscheidung #7 umgesetzt; SSH-Idle-Timeout-Luecke oben gefunden UND behoben; RDP ohne Idle-Timeout unproblematisch. Live-Test aussteht (einzige Moeglichkeit, echte 15+ Minuten Stille zuverlaessig zu pruefen).
Ausgabe in jeder Sitzung vollstaendig vorhanden Kein Datenverlust durch die Architektur zu erwarten (jede Instanz haelt ihren eigenen xterm.js/Guacamole-Zustand, wird nie zerstoert) -- aber nur am echten Terminal pruefbar. Live-Test aussteht.
Eingaben nur in der sichtbaren Sitzung Code vorhanden: SSH bindet ohnehin an sein eigenes Containerelement (kein Aenderungsbedarf); RDP nutzt das _active-Gate (F1) plus keyboard.reset() beim Umschalten (F4). Laut F.5-Risikotabelle das groesste Einzelrisiko der ganzen Stufe. Live-Test aussteht -- dringendste Einzelpruefung vor einer echten Abnahme.
Groesse stimmt nach jedem Umschalten Code vorhanden: setActive(true) loest sendResize()/_sendCurrentSize() aus (F1), Hintergrund-Resize-Events sind seit F4 per _active-Gate unterdrueckt. Live-Test aussteht.
Genau vier sessions-Zeilen mit vier Aufzeichnungen Jede Sitzung erzeugt beim Verbindungsaufbau ihre eigene DB-Zeile + SessionRecorder (unveraendert seit vor Teil F, F.3.10 als harte Regel nie angetastet) -- vier gleichzeitige Instanzen ergeben strukturell vier Zeilen. Live-Test aussteht (Stichprobe in der DB nach dem Lauf).
Zweiter Benutzer unbeeintraechtigt Jede neue Katalog-/Sitzungs-API in F2/F5 ist zwingend auf user_id = eigene beschraenkt (siehe Moduldoc app/catalog/routes.py, dedizierte Tests in test_teil_f_schritt2.py/test_teil_f_schritt5.py fuer Fremdsitzungs-Isolation). active_sessions-Registry ist bereits seit Teil E pro Sitzung, nicht pro Nutzer, geteilt. Live-Test aussteht (echter zweiter Login parallel).

Ehrliches Fazit

Der komplette Code-Stand fuer Teil F (F1-F5) ist fertig, regressionsfrei (146 bestehende + 5 neue Tests bestehen, dieselben 18 laengst bekannten, dokumentierten Fehlschlaege aus FORTSETZUNG_Teil_C.md unveraendert) und gegen jedes einzelne F.6-Kriterium durchgesehen -- dabei wurde eine echte, bis dahin unentdeckte Luecke gefunden und behoben (SSH-Idle-Timeout fuer Hintergrundsitzungen, oben). Was in dieser Sandbox nicht moeglich war und fuer keine Stufe von F1 bis F5 nachgeholt werden konnte: ein echter Browser mit echten SSH-/RDP-Zielsystemen. Alle sieben Kriterien in der Tabelle oben sind auf Code-Ebene plausibel, aber keines ist tatsaechlich durchgespielt worden. Teil F sollte erst dann als im Sinne von F.6 "abgenommen" gelten, wenn eine Session mit Zugriff auf echte SSH-/RDP-Testziele und einen echten Browser das Szenario tatsaechlich einmal durchspielt -- mit besonderem Augenmerk auf Eingabe-Isolation (groesstes Risiko laut F.5) und den 30-Minuten-Idle-Fall (siehe oben).