Commit Graph

516 Commits

Author SHA1 Message Date
Brian Krabach 4ea28d7e1c feat: update serve() to accept and pass TLS SSL params to uvicorn
- Add tls_cert and tls_key keyword arguments to serve() function
- Resolve TLS params via CLI flag > settings.json > empty string default
- Build ssl_kwargs dict conditionally based on file existence check
- Pass ssl_certfile and ssl_keyfile to uvicorn.run() when TLS active
- Print warning 'TLS <path> not found, falling back to HTTP' when files missing
- Print 'https://' URL when TLS active, 'http://' when not
- Add --tls-cert and --tls-key flags to _add_serve_flags() and propagate to serve()
- Add 5 new TLS tests: ssl params passed, no ssl default, fallback warning,
  https URL when active, http URL when inactive
- Update existing serve()-call-signature tests to include tls_cert=None, tls_key=None

Task: task-2-serve-ssl
2026-04-03 22:44:17 -07:00
Brian Krabach 89f39b2366 fix: use compat getattr for not_valid_after/before_utc to support cryptography v41 2026-04-03 22:44:17 -07:00
Brian Krabach 4cca6d1c82 feat: add muxplex.tls module with self-signed cert generation and inspection
- Add muxplex/tls.py with generate_self_signed() and get_cert_info()
- generate_self_signed() creates RSA 2048-bit key + X.509 cert with SAN
  entries (DNS names + 127.0.0.1 + ::1), sets key perms to 0o600,
  creates parent dirs, returns metadata dict
- get_cert_info() inspects PEM certs, returns expires/not_before/
  hostnames/serial, returns None for missing/unreadable files
- Add muxplex/tests/test_tls.py with 9 tests covering all acceptance criteria
- Install cryptography library dependency

Co-authored-by: Amplifier <amplifier@anthropic.com>
2026-04-03 22:44:17 -07:00
Brian Krabach 3a8673690c feat: add tls_cert and tls_key to DEFAULT_SETTINGS
Add two new TLS settings keys to DEFAULT_SETTINGS dict:
- tls_cert: initialized to empty string
- tls_key: initialized to empty string

Keys are placed after federation_key as specified.

Add 5 TDD tests (task-1-tls-settings-keys):
- test_defaults_include_tls_cert
- test_defaults_include_tls_key
- test_load_returns_tls_keys_when_file_missing
- test_tls_keys_patchable
- test_old_settings_file_without_tls_keys_loads_correctly

All 42 tests pass.
2026-04-03 22:44:17 -07:00
Brian Krabach 7b10c61327 docs: TLS setup implementation plans (Phase 1 foundation + Phase 2 auto-detection) 2026-04-03 22:44:17 -07:00
Brian Krabach 1ffffc8d26 docs: add HTTPS/TLS setup design
Design for muxplex setup-tls command with auto-detection:
- Tailscale cert (real LE, universally trusted)
- mkcert (local CA, zero browser warnings)
- Self-signed fallback (works but browser warns)

Covers settings integration, CLI flags, service integration,
doctor diagnostics, and error handling edge cases.
2026-04-03 22:44:17 -07:00
Brian Krabach 834783dec3 fix: manifest.json path, deprecated meta tag, minor code style 2026-04-03 06:03:21 -07:00
Brian Krabach 92a7d63ff5 docs: add federation_key to README settings table + commit CLI plan files
Fixes test_readme_documents_all_settings_keys — federation_key was added
by the federation feature but not documented in README. Also commits the
CLI refactor plan files as historical ADRs.
2026-04-03 05:52:25 -07:00
Brian Krabach 082d03036f fix: preserve federation keys when PATCH sends redacted empty values
The GET /api/settings endpoint redacts remote_instances key fields to ""
for security. When the frontend PATCHes any setting (e.g. device_name) it
sends the full remote_instances array back with those empty strings, which
previously overwrote the real keys on disk.

Fix: in patch_settings(), snapshot existing remote keys by URL before
applying the patch. After merging, restore any key that was emptied by
redaction — identified by url match + empty/missing key in patch. Only a
non-empty key in the patch is treated as an intentional key rotation.

Also adds 5 regression tests covering:
- Empty key in patch preserves existing key (core redaction bug)
- Non-empty key in patch overwrites old key (intentional rotation)
- Missing key field in patch preserves existing key
- Multi-remote list: all keys preserved when only one field changes
- New remote with a key is saved correctly
2026-04-02 20:20:32 -07:00
Brian Krabach b0c1d0ab8d fix: remoteId=0 treated as falsy — use null checks instead of || for integer remoteId
remoteId is an integer index (0, 1, 2...) returned by /api/federation/sessions.
In JavaScript, 0 is falsy, so all || '' patterns converted remoteId=0 to '' —
making the first remote device (ALIENWARE, index 0) appear as a local session.

The symptom: clicking an ALIENWARE session from spark-1 or spark-2 sent
  POST /api/sessions/{name}/connect   ← local endpoint, session doesn't exist → 404
instead of
  POST /api/federation/0/connect/{name}  ← correct federation proxy route

Only affected the FIRST remote (index 0). spark-1 ↔ spark-2 worked because
each sees the other as remoteId=1.

Locations fixed in frontend/app.js:
  1. buildTileHTML: session.remoteId ? → session.remoteId != null ?
  2. buildSidebarHTML: session.remoteId || '' → session.remoteId != null ? ... : ''
  3. _previewClickHandler: session && session.remoteId || '' → null-safe ternary
  4. openSession _viewingRemoteId: opts.remoteId || '' → null-safe ternary
  5. openSession _remoteId: opts.remoteId || '' → null-safe ternary
  6. openSession routing: if (_remoteId) → if (_remoteId !== '')
  7. closeSession DELETE guard: if (!_viewingRemoteId) → if (_viewingRemoteId === '')

DOM dataset reads (tile.dataset.remoteId, item.dataset.remoteId) are unaffected
because dataset values are always strings — '0' is truthy, so || '' worked there.

Tests added: 5 new tests covering all four code paths (buildTileHTML,
buildSidebarHTML, openSession connect routing, closeSession DELETE skip)
with integer remoteId=0. Total: 291 → 296 tests, 0 failures.
2026-04-02 04:46:50 -07:00
Brian Krabach 000c71c40d fix: prevent service crash-loop on port-in-use at startup
serve() now kills any stale process holding the configured port before
uvicorn tries to bind. Prevents the crash-loop where systemd restarts
muxplex but the old process is still holding port 8088 (observed:
2075+ restarts before manual intervention).

New helper _kill_stale_port_holder(port):
- Runs lsof -ti :<port> to find occupying PIDs
- Sends SIGTERM to all foreign PIDs (skips own PID)
- Waits 1 second for the port to free
- Silently swallows all errors (missing lsof, permission denied)
  so a broken environment never prevents startup

Also adds TimeoutStopSec=10 and KillMode=mixed to the systemd unit
template so the old process gets SIGKILL'd if it does not exit on
SIGTERM within 10 seconds — preventing the SIGTERM-ignored zombie
scenario entirely.
2026-04-02 00:51:20 -07:00
Brian Krabach b310b7f8a6 fix: self-host xterm.js vendor libs — eliminates Edge Tracking Prevention console noise 2026-04-01 20:40:14 -07:00
Brian Krabach 463cbf4cb3 fix: federation remoteId must be integer index, not URL string
- fetch_remote now accepts index i from enumerate() and sets remote_id: int = i
  instead of remote.get("id", url), which fell back to the URL string
- asyncio.gather uses enumerate(remote_instances) to pass the integer index
- federation_connect route: remote_id type changed from str to int; FastAPI
  now validates path param at framework level, simplifying the handler body
- Update test_federation_sessions_includes_remote_failure_status to expect
  integer 0 (not the legacy 'remote-1' id field string)
- Update test_federation_connect_returns_404_for_non_integer_remote_id: now
  expects 422 (FastAPI schema validation) instead of 404
- Add test_federation_sessions_remote_id_is_integer_index verifying remoteId
  is 0 for first remote and 1 for second
2026-04-01 20:22:45 -07:00
Brian Krabach a07e5b5383 merge: integrate upstream (clipboard, URL click, search addon, delete fixes) with federation proxy rewrite 2026-04-01 18:55:54 -07:00
Brian Krabach e4ba201cd2 refactor: use module-level socket import in _ttyd_is_listening 2026-04-01 18:34:12 -07:00
Brian Krabach dae5cea16b chore: remove unused websockets.exceptions import, add .gitignore, untrack __pycache__
- Remove unused 'import websockets.exceptions' from main.py (no reference
  to websockets.exceptions.* anywhere in the file; all WS handlers use
  broad Exception catches)
- Add .gitignore with __pycache__/, *.pyc, .venv/, and other standard
  Python excludes so bytecode no longer shows as modified in git status
- Untrack all pre-existing __pycache__/*.pyc files via git rm --cached
2026-04-01 18:30:10 -07:00
Brian Krabach e738701cdd chore: final Phase 2 cleanup — remove stale references and unused imports
Remove unused JSONResponse import from starlette.responses in main.py.
All other stale references (CORSMiddleware, federation_tokens, X-Muxplex-Token,
window.opener, etc.) verified absent. Federation proxy endpoints confirmed present.
2026-04-01 18:17:55 -07:00
Brian Krabach ebbf03118b fix: reorganize settings tabs — merge notifications into sessions, move device-name to display, remove view-scope 2026-04-01 18:02:07 -07:00
Brian Krabach 805d3d7898 fix: restore notifications tab and revert out-of-scope settings dialog reorganization
Revert index.html to state at 046d123 (task-7 completion), undoing the
out-of-scope settings dialog reorganization from 9f51d1b that:
- Removed the notifications tab button and panel
- Moved #setting-bell-sound into sessions panel
- Moved #setting-device-name into display panel
- Moved #notification-status-text and #notification-request-btn into sessions panel
- Removed #setting-view-scope from devices panel

This restores the 5-tab structure (display, sessions, notifications,
new-session, devices) and correct element placement that the Python
HTML tests assert.

Also remove 2 stale test_frontend_js.py tests that expected buildSources
and _sources to exist after task-1 removed them:
- test_save_remote_instances_rebuilds_sources
- test_build_sources_checks_multi_device_enabled

All 770 Python tests now pass (was 10 failures).
2026-04-01 17:50:16 -07:00
Brian Krabach 9f51d1bd51 fix: reorganize settings dialog — remove notifications tab, move bell/device-name fields
🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-04-01 17:40:05 -07:00
Brian Krabach 046d123149 refactor: remove all cross-origin browser-direct federation code
- Delete storeFederationToken function from app.js
- Delete window.addEventListener('message',...) handler from app.js
- Delete buildAuthTileHTML function from app.js
- Delete formatLastSeen function from app.js
- Delete buildOfflineTileHTML function from app.js
- Delete openLoginPopup function from app.js
- Simplify api() to same-origin only (no baseUrl/credentials/X-Muxplex-Token)
- Remove federation auth relay <script> block from index.html
- Remove /api/auth/token endpoint from main.py
- Remove CORSMiddleware from main.py
- Remove X-Muxplex-Token header check from auth.py
- Delete corresponding tests and add absence verification tests
2026-04-01 17:30:18 -07:00
Brian Krabach c48456a14a fix: include .settings-remote-key in remote instance debounced input selector 2026-04-01 17:08:14 -07:00
Brian Krabach 4ccaaf153e fix: parse JSON response in federation generate-key handler
api() returns a Response object; chain .then(res => res.json()) before
accessing data.key so the returned key is displayed in #federation-key-display
and settings-key-display--visible is applied.
2026-04-01 16:57:15 -07:00
Brian Krabach 63e7ef2a1c feat: federation key management UI in Settings > Multi-Device 2026-04-01 16:50:01 -07:00
Brian Krabach 77e2b8476b fix: revert sidebar to single-line header (name + badge + ×) 2026-04-01 16:36:56 -07:00
Brian Krabach c5df6511c1 style: fix stale comment, merge split tile-meta CSS block, clarify test variable 2026-04-01 16:29:33 -07:00
Brian Krabach f18caf5d27 fix: move device badge inside tile-meta to prevent × overlap 2026-04-01 16:21:57 -07:00
Brian Krabach adb0f8c39c feat: upgrade xterm-addon-image 0.5.0 → @xterm/addon-image 0.9.0
Newer version has improved Sixel rendering and image lifecycle management.
UMD global (window.ImageAddon) is compatible — no JS code changes needed.
Should improve image replacement behavior (stacking issue with yazi).
2026-04-01 16:18:58 -07:00
Brian Krabach d7bcf49283 fix: use var(--border) for default left border on tiles and sidebar items 2026-04-01 16:12:23 -07:00
Brian Krabach d7d40f1f0d refactor: remove sourceUrl property from session objects 2026-04-01 16:04:53 -07:00
Brian Krabach b7cf45c621 test: add dedicated unit tests for buildStatusTileHTML
Add 4 direct unit tests for buildStatusTileHTML to match coverage
pattern of sibling functions buildAuthTileHTML and buildOfflineTileHTML:
- is exported as a function
- returns article element with correct statusClass
- escapes XSS in deviceName
- renders statusText in badge span
2026-04-01 15:56:27 -07:00
Brian Krabach 84b277854b refactor: remove _sources state and buildSources/tagSessions/mergeSources 2026-04-01 15:43:51 -07:00
Brian Krabach 7132587847 feat: add search (Ctrl+F) and image rendering (Sixel/Kitty) addons
Search: xterm-addon-search (0.13.0) with slide-down search bar. Ctrl+F
opens bar; type to search (live highlight); Enter/Shift+Enter for
next/prev; Escape to close. Bar sits above terminal-container inside
a new .terminal-wrapper flex column.

Image: xterm-addon-image (0.5.0) for inline graphics. Enables Sixel,
iTerm2 IIP, and Kitty graphic protocols — needed for tools like yazi
file manager. Auto-detects escape sequences, no configuration needed.
2026-04-01 14:59:43 -07:00
Brian Krabach 235b4aadfe fix: guard negative remote_id in federation_connect endpoint
Negative Python list indices bypass the intended 404 guard — e.g.
remote_id=-1 on a non-empty list silently proxied to the last remote
instead of returning 404.

Add an explicit 'idx < 0' check before the list lookup, consistent
with the sibling federation_terminal_ws_proxy endpoint (line 728).

Also adds test_federation_connect_returns_404_for_negative_remote_id
to document the expected behavior and prevent regression.
2026-04-01 14:38:09 -07:00
Brian Krabach 2c30ad39a2 feat: clickable URLs in terminal — Ctrl/Cmd+Click opens in new tab
Loads xterm-addon-web-links (0.9.0) from CDN. URLs in terminal output
are auto-detected and underlined on hover. Ctrl+Click (Linux/Windows)
or Cmd+Click (macOS) opens the URL in a new browser tab. Plain click
preserved for normal terminal text selection.
2026-04-01 14:34:18 -07:00
Brian Krabach 745e58bae6 style: add 503 warning log and regroup 502 test with federation_connect suite 2026-04-01 14:33:27 -07:00
Brian Krabach b1639826ab fix: improve fetch_remote error handling and diagnostics
- Add _log.warning() to bare except clause in fetch_remote so unexpected
  errors (JSON parse failures, AttributeError, etc.) leave a diagnostic
  trail instead of silently surfacing as 'unreachable'
- Map HTTPStatusError (non-401/403 HTTP errors like 500, 503, 429) to
  'unreachable' instead of 'auth_failed' — semantically correct since
  these are connectivity/server errors, not authentication failures
- Fix stale task comment in test_api.py: task-8 → task-5
2026-04-01 14:22:29 -07:00
Brian Krabach 489a127056 refactor: defensive getattr guard and clarified cleanup comment in lifespan
- Use getattr(app.state, 'federation_client', None) in shutdown to guard
  against the theoretical case where AsyncClient constructor raised before
  setting app.state.federation_client
- Rename 'Shutdown:' comment to 'Cleanup:' in the finally block to better
  describe that the block covers both the poll-task cancellation and the
  overall cleanup guarantee
2026-04-01 14:13:45 -07:00
Brian Krabach 43cc75efc8 test: add dispatch test for generate-federation-key subcommand 2026-04-01 14:03:07 -07:00
Brian Krabach 939a20e8b5 fix: migrate test_app.mjs sourceUrl tests to remoteId API
Update 9 test_app.mjs tests that regressed when task-17 migrated
from sourceUrl to remoteId throughout app.js:

- buildTileHTML: assert data-remote-id instead of data-source-url
- buildTileHTML data-session-key: assert data-remote-id
- buildSidebarHTML: assert data-remote-id instead of data-source-url
- buildSidebarHTML empty: assert data-remote-id="" not data-source-url=""
- _previewClickHandler: assert remoteId forwarded not sourceUrl
- openSession with remoteId: assert federation proxy URL instead of remote URL
  (no credentials: include - same-origin calls don't need it)
- openSession passes remoteId to _openTerminal: assert fed-abc123
- closeSession remote: use remoteId to set _viewingRemoteId state
- cycleViewMode state pollution fixed as side-effect of above fixes
  (failing tests no longer leave document.getElementById mocked)
2026-04-01 13:35:26 -07:00
Brian Krabach 38300fcf79 feat: route remote terminal connections through federation proxy
- connectWebSocket() now accepts remoteId instead of sourceUrl; when
  remoteId is set the WebSocket URL is ws://host/federation/{remoteId}/terminal/ws
  (same-origin, no cross-origin connections)
- openTerminal() signature updated to accept remoteId parameter
- openSession() in app.js routes remote connect POST to
  /api/federation/{remoteId}/connect/{name} instead of the remote URL
- window._openTerminal() call passes remoteId instead of sourceUrl
- _viewingSourceUrl state replaced with _viewingRemoteId
- Tile and sidebar click handlers updated to pass remoteId
- HTML attributes changed from data-source-url to data-remote-id
- Test suite updated: removed cross-origin sourceUrl tests, added
  federation proxy path tests
2026-04-01 13:18:10 -07:00
Brian Krabach 7e28438953 fix: update pollSessions JSDoc and remove zombie backoff test 2026-04-01 13:08:31 -07:00
Brian Krabach dc7c077b6a test: remove stale multi-source pollSessions tests superseded by task-16 2026-04-01 13:01:09 -07:00
Brian Krabach 2de19bd988 fix: clean exit on Ctrl+C in muxplex service logs
subprocess.run with check=True raised CalledProcessError then
KeyboardInterrupt on Ctrl+C, printing an ugly traceback. Fix: remove
check=True, catch KeyboardInterrupt, exit silently.
2026-04-01 13:00:00 -07:00
Brian Krabach 442cfcc051 feat: simplify pollSessions to use federation proxy endpoint 2026-04-01 12:54:08 -07:00
Brian Krabach bdb3f069e5 fix: delete active session returns to dashboard, sidebar/tile delete doesn't navigate
Bug 1: killSession() now checks if _viewingSession === name and calls
closeSession() before pollSessions(). Previously the user was stuck in
the expanded terminal view for a dead session after deleting it.

Bug 2: Sidebar item click handler now ignores clicks on .sidebar-delete
buttons. Tile click handler now ignores clicks on .tile-delete buttons.
Previously stopPropagation() in the document-level delete handler fired
too late (after the sidebar-item/tile handlers had already called
openSession), causing the session to be navigated to before being killed.

Tests added:
- killSession closes active session and returns to dashboard
- sidebar click handler ignores clicks on delete button
- tile click handler ignores clicks on tile-delete button

321 JS tests pass, 760 Python tests pass.
2026-04-01 12:48:38 -07:00
Brian Krabach 0e3ed96c16 refactor: drop unused css=None parameters in 3 CSS test functions
test_css_session_grid, test_css_tile_height, and test_css_bell_indicator
each declared a css=None parameter that was immediately overwritten with
read_css(). The parameter was dead API surface — never passed by any caller.
Removed the parameter from all three signatures.
2026-04-01 12:48:00 -07:00
Brian Krabach 56694fde61 style: add debug logging to ws relay exceptions; format test_frontend_css 2026-04-01 12:44:31 -07:00
Brian Krabach 2dda544893 chore: remove untracked planning doc swept up in phase 1 commit 2026-04-01 12:37:53 -07:00
Brian Krabach d0fe9cda67 chore: Phase 1 complete — all backend proxy tests pass 2026-04-01 12:33:17 -07:00