umbau 1.0

This commit is contained in:
2026-09-02 20:30:44 +02:00
parent afe6719f51
commit 5c95b21be7
77 changed files with 10733 additions and 1914 deletions

742
FORTSETZUNG_Teil_F.md Normal file
View File

@ -0,0 +1,742 @@
# 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).