Commit Graph

452 Commits

Author SHA1 Message Date
Brian Krabach 23a6dfac31 feat: add PyPI metadata — authors, classifiers, keywords 2026-04-04 14:09:33 -07:00
Brian Krabach 547c89ac6d docs: add search, clickable URLs, image rendering to README 2026-04-04 09:23:24 -07:00
Brian Krabach 1fd8bf46e8 docs: add bell-clear, device selector, activity title, favicon badge to README + move branding writeup 2026-04-04 09:16:08 -07:00
Brian Krabach b333c1fccd fix: update createNewSession regex tests to match 2-param signature
The 5 pre-existing tests used \(\w+\) (single-param regex) but Task 4
changed createNewSession(name) → createNewSession(name, remoteId). Updated
regex to \([\w,\s]+\) to match one or more comma-separated parameters.
2026-04-04 08:34:36 -07:00
Brian Krabach 9aac14212a chore: lint/format fixes 2026-04-04 08:22:36 -07:00
Brian Krabach be1913849e feat: style device select dropdown for new session dialog 2026-04-04 08:16:33 -07:00
Brian Krabach 44e47c5228 feat: route createNewSession through federation proxy for remote devices
- Accept remoteId parameter (defaults to '' via remoteId = remoteId || '')
- Compute endpoint: remoteId ? '/api/federation/{remoteId}/sessions' : '/api/sessions'
- Match sessions in poll loop via sessionKey (with fallback to name)
- Pass { remoteId } option to openSession for remote auto-open
- Add 3 tests verifying federation routing, remoteId passthrough, and sessionKey matching
2026-04-04 08:09:02 -07:00
Brian Krabach c28cb0a7f9 feat: add device select to mobile FAB session input
- Update showFabSessionInput to call _createDeviceSelect() for optional
  device <select> element
- Append select (if present) then input to overlay
- Enter handler reads remoteId from select.value, passes to createNewSession
- Add test: showFabSessionInput creates device select when multi_device_enabled with remotes
2026-04-04 07:59:43 -07:00
Brian Krabach e12a965cdf feat: add device select dropdown to showNewSessionInput
- Add _createDeviceSelect() helper function after _createSessionInput()
- Returns null when multi_device_enabled is false or remote_instances is empty
- Creates <select class='new-session-device-select'> with local + remote options
- Pre-selects based on _activeFilterDevice match
- Update showNewSessionInput() to call _createDeviceSelect() and insert select
- cleanup() removes both select and input; restores btn display
- Enter handler reads remoteId from select.value, calls createNewSession(name, remoteId)
- Export _createDeviceSelect for testing
- Add 3 tests covering select creation, integration, and argument passing
2026-04-04 07:51:21 -07:00
Brian Krabach 97f59fda84 feat: add POST /api/federation/{remote_id}/sessions proxy endpoint
Adds federation_create_session endpoint following the same pattern as
federation_connect and federation_bell_clear. The endpoint:
- Looks up remote by integer index into remote_instances in settings
- Sends POST {remote_url}/api/sessions with Bearer auth header and
  JSON body {name: ...}
- Returns remote's JSON response
- Raises 404 for invalid remote_id, 503 when unreachable, 502 on HTTP error

Adds 4 tests covering:
- Proxy behavior with correct URL, auth header, and JSON body forwarding
- 404 for out-of-range remote_id
- 503 when ConnectError raised
- 502 when remote returns HTTP error status
2026-04-04 07:43:18 -07:00
Brian Krabach 02f1e23f35 style: auto-format test_frontend_js.py with ruff 2026-04-04 07:11:04 -07:00
Brian Krabach 094ccd7d81 feat: fire bell-clear POST when opening remote sessions 2026-04-04 07:06:53 -07:00
Brian Krabach be7663ca57 feat: use sessionKey for bell comparison in pill badges
Update updatePillBell and updateSessionPill to build a viewingKey
that accounts for remote sessions (_viewingRemoteId prefix) and
compare using s.sessionKey || s.name instead of s.name alone.

This prevents false bell suppression when a local session shares a
name with a remote session — the two are now distinguished by their
fully-qualified key (remoteId:name vs plain name).
2026-04-04 06:58:31 -07:00
Brian Krabach 6ee6112744 feat: add federation bell/clear proxy endpoint 2026-04-04 06:52:22 -07:00
Brian Krabach 5f9d6a593e feat: add POST /api/sessions/{name}/bell/clear endpoint 2026-04-04 06:46:31 -07:00
Brian Krabach 42142c0954 feat: inject sessionKey for remote federation sessions
- Remote sessions now include sessionKey field formatted as 'remoteId:name'
- Local sessions are unchanged (no sessionKey) as they never collide
- Prevents name collisions between local and remote sessions with identical names
- Added tests: test_federation_sessions_local_sessions_have_no_session_key
               test_federation_sessions_remote_sessions_have_session_key
2026-04-04 06:36:24 -07:00
Brian Krabach 28aaf3d484 fix: restore favicon activity badge + add activity count and hostname to page title
Favicon badge: removed crossOrigin='anonymous' from _drawFaviconBadge Image
lazy-init. Same-origin favicons don't need CORS credentials; setting it caused
silent cache-miss failures when the browser had the asset cached without CORS
headers, preventing the canvas from drawing.

Page title: new updatePageTitle() centralizes title logic. Format:
'(N) hostname - muxplex' when N sessions have unseen bells, otherwise
'hostname - muxplex'. Hostname uses device_name setting with fallback
to location.hostname. Called from pollSessions on every tick, from
loadServerSettings callback, and immediately when device_name input changes
(updating _serverSettings.device_name in place first so the helper sees the
latest value without waiting for the debounced PATCH).

Replaced 3 scattered document.title assignments with updatePageTitle().
Updated 3 existing source-pattern tests to reflect the new centralised
approach; added 2 new tests for updatePageTitle existence and pollSessions
call site.
2026-04-04 06:21:13 -07:00
Brian Krabach c92689dab2 feat: TLS nudge in doctor and service install output
When host is set to network access (not 127.0.0.1) and TLS is not
configured, doctor shows 'Run: muxplex setup-tls' and service install
shows a tip line. Hidden on localhost-only setups since clipboard
works without HTTPS there.
2026-04-04 03:40:44 -07:00
Brian Krabach 7db56e600f docs: TLS nudge design — contextual hints in doctor + service install 2026-04-04 03:29:36 -07:00
Brian Krabach cb2d425fa7 fix: Tailscale detection reads DNSName from Self + suppress cryptography deprecation
Bug 1: detect_tailscale() looked for DNSName at top level of JSON but
tailscale status --self --json nests it inside Self.DNSName. Fixed to
read from Self first with top-level fallback.

Bug 2: getattr(cert, 'not_valid_after_utc', cert.not_valid_after)
eagerly evaluated the fallback, triggering CryptographyDeprecationWarning.
Replaced with try/except AttributeError pattern in all three occurrences
(generate_self_signed and get_cert_info).
2026-04-04 03:07:13 -07:00
Brian Krabach ddba3baeb1 fix: add cryptography to pyproject.toml dependencies
muxplex/tls.py imports cryptography for self-signed cert generation
but it was missing from the declared dependencies. uv tool installs
would fail with ModuleNotFoundError on setup-tls.
2026-04-04 02:40:19 -07:00
Brian Krabach 4483d879c6 test: fix misleading docstrings and tighten detection-priority assertion in test_readme.py
- test_readme_tls_cert_has_empty_default: docstring now accurately describes
  the assertion (key presence check, not empty-string default verification)
- test_readme_tls_key_has_empty_default: same correction
- test_readme_tls_detection_priority_explanation: replace broad three-way or
  (detection/priority/auto-detect) with tighter check requiring both 'Tailscale'
  and 'mkcert' to appear alongside priority language, so incidental word matches
  can no longer satisfy the assertion
2026-04-03 23:52:53 -07:00
Brian Krabach 1b92340975 docs: update README with complete Phase 2 TLS documentation
- Update HTTPS/TLS feature section: remove (Phase 2) placeholders,
  document Tailscale (tailscale cert), mkcert (zero browser warnings),
  and self-signed (browser warning) methods with accurate descriptions
- Update HTTPS/TLS setup examples: add --method tailscale/mkcert/selfsigned
  and --status commands, detection priority explanation paragraph,
  and 90-day Tailscale cert renewal note
- Update CLI reference: change description to Tailscale/mkcert/self-signed,
  add setup-tls --status entry with 'Show current TLS configuration'
- Update tests: replace Phase-2-placeholder test with 10 new tests
  covering all Phase 2 requirements (no placeholders, all methods,
  status command, detection priority, cert renewal note)
2026-04-03 23:45:25 -07:00
Brian Krabach a4d6e7a348 test: add test_generate_mkcert_includes_tailscale_sans integration test
Verifies that generate_mkcert() correctly passes Tailscale hostnames
(spark-1.tail8f3c4e.ts.net, 100.64.0.1) through to the mkcert command
arguments when extra_hostnames is provided.

- fake_run tracks gen_calls only for '-cert-file' invocations
- Creates fake cert/key files to simulate mkcert success
- Asserts result is not None
- Asserts Tailscale hostname appears in mkcert command args

Part of Phase 2 full test sweep (task-7).
2026-04-03 23:36:46 -07:00
Brian Krabach aa1dadf834 feat: add existing cert detection and regenerate prompt to setup-tls
- Added existing cert detection at the top of setup_tls() in muxplex/cli.py
- After defining cert_path/key_path, checks if tls_cert and tls_key are both set in settings AND the cert file exists
- If so, calls get_cert_info() and prints 'TLS already configured (expires YYYY-MM-DD).'
- Prompts user with input('Regenerate? [y/N] ') with try/except for EOFError and KeyboardInterrupt (defaults to 'n')
- If user does not answer 'y' or 'yes', prints 'Keeping existing certificates.' and returns early
- Added two new tests in test_cli.py: test_setup_tls_prompts_when_certs_exist and test_setup_tls_regenerates_on_eof
- All 866 tests pass

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-04-03 23:28:59 -07:00
Brian Krabach 52d0021f06 feat: add setup_tls_status() and --status flag to setup-tls subcommand
- Added setup_tls_status() function to display current TLS configuration status
- Shows cert path, key path, hostnames, expiry, and status when configured
- Shows 'not configured' prompt when TLS is not set up
- Shows 'configured but cert not readable' when cert exists in settings but file isn't accessible
- Added --status flag to setup-tls argparse subparser
- Updated main() dispatch to call setup_tls_status() when --status flag is passed
- Added 4 new tests covering all scenarios

All 95 tests pass.
2026-04-03 23:21:05 -07:00
Brian Krabach 3bba2d44e2 feat: auto-detection chain in setup_tls() — Tailscale → mkcert → self-signed
Rewrite setup_tls() with full auto-detection chain:
1. Tailscale: detect via detect_tailscale(), generate cert with generate_tailscale()
2. mkcert: detect via detect_mkcert(), generate cert with generate_mkcert()
   - Includes optional Tailscale SANs as extra_hostnames when available
3. Self-signed: fallback via generate_self_signed()

For forced methods (--method tailscale/mkcert), exits with code 1 on failure.
Prints cert info (Certificate, Key, Hostnames, Expires) on success.
Method-specific warnings: self-signed (browser warning), tailscale (90-day expiry).
Always prints 'Restart service to apply: muxplex service restart'.

Update argparse --method choices to include 'tailscale' and 'mkcert'.

Add 4 tests:
- test_setup_tls_auto_uses_tailscale_when_available
- test_setup_tls_auto_falls_to_mkcert_when_no_tailscale
- test_setup_tls_auto_falls_to_selfsigned_when_nothing_available
- test_setup_tls_method_choices_expanded

Co-authored-by: Amplifier <amplifier@bkrabach.com>
2026-04-03 23:13:50 -07:00
Brian Krabach 708dffe20e feat: add detect_mkcert() and generate_mkcert() for locally-trusted CA certificates
- detect_mkcert() returns True if mkcert is on PATH via shutil.which
- generate_mkcert() runs mkcert -install then generates cert for hostname,
  hostname.local, localhost, 127.0.0.1, ::1, plus any extra_hostnames;
  deduplicates preserving order; sets key permissions to 0o600
- Returns None on install failure, subprocess error, or missing output files
- Added 4 tests: detect true/false, generate success/install-fails
2026-04-03 23:03:56 -07:00
Brian Krabach 33b9ebb392 feat: add generate_tailscale() for Tailscale cert generation
Add generate_tailscale(cert_path, key_path, hostname) to muxplex/tls.py.
The function calls 'tailscale cert' to obtain real Let's Encrypt certificates
via the Tailscale CLI.

- Creates parent directories with mkdir(parents=True, exist_ok=True)
- Runs subprocess with --cert-file and --key-file flags (timeout=30)
- Returns None on non-zero exit, TimeoutExpired, OSError, or missing files
- On success: sets key_path chmod(0o600), reads cert info, returns metadata dict
  with method='tailscale', cert_path, key_path, hostnames, expires

Tests added:
- test_generate_tailscale_calls_tailscale_cert: verifies --cert-file/--key-file flags
- test_generate_tailscale_returns_none_on_failure: verifies None on rc=1
2026-04-03 22:56:45 -07:00
Brian Krabach 1e20d5b131 feat: add detect_tailscale() to tls.py with 4 tests (task-1-tailscale-detection)
- Probe Tailscale CLI via shutil.which('tailscale')
- Run 'tailscale status --self --json' with 10-second timeout
- Return dict(hostname, ips, cert_domains) on success
- Return None if not installed, non-zero exit, timeout/JSON/OS error,
  empty CertDomains, or empty DNSName
- All imports (json, shutil, subprocess) inside function body
- 4 tests: available, not installed, not connected, no cert domains
2026-04-03 22:51:34 -07:00
Brian Krabach adddc68758 docs: add TLS setup section to README 2026-04-03 22:44:17 -07:00
Brian Krabach b51463a5c4 test: code quality fixes — docstring count, ruff format, test name accuracy
- Update test_tls.py module docstring: '9 tests' → '12 tests' (stale after edge case additions)
- Parenthesize assertion message on line 177 of test_tls.py (ruff line-length format)
- Rename test_serve_warns_when_only_cert_set → test_serve_no_ssl_when_only_cert_set
  to accurately reflect test behavior (verifies no SSL, not that a warning is printed)
2026-04-03 22:44:17 -07:00
Brian Krabach c2c916077e test: add edge case tests for TLS certificate generation and SSL serve behavior 2026-04-03 22:44:17 -07:00
Brian Krabach fad9c10bbb feat: add TLS status section to doctor() command output
- Inserts TLS check between Serve config and Auth status sections
- Shows enabled status with cert expiry date when valid certs configured
- Shows warning with days-expired count when cert is expired
- Shows configured-but-not-readable warning when cert file unreadable
- Shows disabled warning mentioning clipboard/HTTPS requirement when no TLS
- Adds 3 new tests: tls_disabled, tls_enabled, tls_clipboard_warning
- All 86 CLI tests pass

Co-authored-by: Amplifier <amplifier@amplified.dev>
2026-04-03 22:44:17 -07:00
Brian Krabach 41a3adbf53 feat: add setup-tls subcommand for TLS certificate configuration
- Adds setup_tls() function to generate self-signed certificates in config dir
- Registers 'setup-tls' subcommand with --method argument (auto/selfsigned, default: auto)
- Automatically updates settings with cert/key paths after generation
- Outputs summary with self-signed warning and restart hint
- Adds 3 tests: subcommand registration, dispatch flow, and cert generation
- All 83 tests pass

Generated with Amplifier
2026-04-03 22:44:17 -07:00
Brian Krabach f6316f029d test: add TLS CLI flag tests for task-3-cli-flags
Add three tests verifying --tls-cert and --tls-key flag behavior:
- test_main_passes_tls_cert_and_key_flags: verifies exact kwargs forwarded to serve()
- test_main_passes_none_for_unset_tls_flags: verifies None when flags omitted
- test_serve_subcommand_accepts_tls_flags: verifies 'muxplex serve --tls-cert ... --tls-key ...'

All three tests patch muxplex.cli.serve and sys.argv per spec.
CLI implementation was already complete (--tls-cert/--tls-key in _add_serve_flags
and forwarded in main() else block). All 80 tests pass.
2026-04-03 22:44:17 -07:00
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