# Server UI — CLI (terminal PTY) **Data:** 2026-07-05 **Status:** wdrożone (bugfix FitAddon: 2026-07-05; pamięć sesji: 2026-07-05) Zakładka **CLI** w panelu Server UI (`:8091`) — interaktywny shell bash w przeglądarce (PTY + xterm.js). ## UI - Zakładka **CLI** (pierwsza, przed Pliki) - Hash URL: `http://:8091/#cli` - Wymaga **API Key** w toolbarze (WebSocket nie obsługuje nagłówka `X-API-Key`) ## Architektura ```mermaid sequenceDiagram participant Browser participant xterm as xterm_js participant WS as WS_api_cli_ws participant PTY as cli_pty_py participant Shell as bash Browser->>xterm: klawiatura xterm->>WS: dane terminala WS->>PTY: zapis master_fd PTY->>Shell: stdin Shell->>PTY: stdout/stderr PTY->>WS: odczyt master_fd WS->>xterm: output ``` ## WebSocket | Parametr | Wartość | |----------|---------| | Ścieżka | `/api/cli/ws` | | Auth | `?api_key=` (query string) | | Reattach | `?session_id=` (opcjonalnie, z `sessionStorage`) | | Protokół I/O | surowe bajty terminala (text/binary frames) | | Resize | JSON: `{"type":"resize","cols":120,"rows":40}` | | Zamknij sesję | JSON: `{"type":"close"}` (klient → serwer) | ### Handshake sesji (serwer → klient) | Payload | Znaczenie | |---------|-----------| | `{"type":"session","id":""}` | Nowa sesja — ID zapisane w `sessionStorage` (`server-ui-cli-session-id`) | | `{"type":"replay","complete":true}` | Koniec replay bufora po reattach | ### Przykład (wscat) ```bash API_KEY=$(sudo grep ^API_KEY= /opt/control-plane/.env | cut -d= -f2) # wymaga: npm i -g wscat wscat -c "ws://127.0.0.1:8091/api/cli/ws?api_key=${API_KEY}" ``` Bez klucza na bind LAN → połączenie zamknięte z kodem `1008`. ## Konfiguracja W [`/opt/control-plane/.env`](/opt/control-plane/.env) (opcjonalnie): | Zmienna | Domyślnie | Opis | |---------|-----------|------| | `CLI_ENABLED` | `1` | `0` = WebSocket odrzucany | | `CLI_SHELL` | `/bin/bash` | Shell PTY (np. `/bin/bash -l`) | | `CLI_DEFAULT_CWD` | `$HOME` usługi | Katalog startowy | | `CLI_MAX_SESSIONS` | `5` | Limit równoległych sesji PTY (bash) | | `CLI_SESSION_TTL` | `300` | Sekundy utrzymania odłączonej sesji przed kill | ## Pamięć sesji - **Przełączanie zakładek** — WebSocket i bash pozostają aktywne (bez `disconnectCli`) - **F5 / reload** — `session_id` z `sessionStorage` → reattach do tego samego bash (bufor ~256 KB replay) - **Połącz ponownie** — wysyła `{"type":"close"}`, czyści `sessionStorage`, nowy bash - **Zamknięcie karty** — serwer utrzymuje PTY przez `CLI_SESSION_TTL`, potem zwalnia slot ```mermaid sequenceDiagram participant Browser participant SS as sessionStorage participant WS as WebSocket participant Mgr as CliSessionManager participant Bash as bash Browser->>WS: connect api_key WS->>Mgr: create_session Mgr->>Bash: spawn PTY Mgr-->>Browser: session id Browser->>SS: zapisz session_id Note over Browser,Bash: przełączenie zakładki — WS żyje Browser->>Browser: F5 reload Browser->>SS: odczyt session_id Browser->>WS: connect session_id WS->>Mgr: attach_session Mgr-->>Browser: replay + live output ``` ## Uprawnienia POSIX Server UI działa jako **`tomasz-syn-grzegorza`** (`server-ui.service`): - `docker ps`, edycja repo, `/data/apps/` — OK - `sudo` — interaktywnie (hasło użytkownika w terminalu) - root-only pliki — odmowa jak w SSH ## Kod | Plik | Rola | |------|------| | [`stacks/server-ui/cli_pty.py`](../stacks/server-ui/cli_pty.py) | `CliSessionManager` — PTY, detach/attach, bufor outputu | | [`stacks/server-ui/app.py`](../stacks/server-ui/app.py) | WebSocket `/api/cli/ws`, mount `/static` | | [`stacks/server-ui/static/index.html`](../stacks/server-ui/static/index.html) | Zakładka CLI | | [`stacks/server-ui/static/vendor/xterm/`](../stacks/server-ui/static/vendor/xterm/) | xterm.js + FitAddon (vendored; UMD: `new FitAddon.FitAddon()`) | ## Bezpieczeństwo - Pełny shell przez LAN = **wysokie ryzyko** — wymagany `API_KEY` przy `SERVER_UI_HOST=0.0.0.0` - `CLI_ENABLED=0` jako kill switch - `CLI_MAX_SESSIONS` ogranicza liczbę równoległych bashów (PTY, nie połączeń WS) - Brak whitelisty komend (właściciel serwera) - Jawne zamknięcie: przycisk **Połącz ponownie** lub `{"type":"close"}` - Odłączenie WS (reload) → detach + TTL; reattach w tej samej karcie przeglądarki ## Deploy ```bash sudo rsync -a --exclude '.venv' --exclude '__pycache__' \ stacks/server-ui/ /opt/server-ui/ sudo systemctl restart server-ui ``` Tutorial użytkownika: [`manual-tutorial/10-server-ui-cli.md`](../manual-tutorial/10-server-ui-cli.md) ## Znane problemy / bugfix Pusty terminal przy działającym API Key: [`SERVER-UI-CLI-BUGFIX.md`](SERVER-UI-CLI-BUGFIX.md)