- Add .github/workflows/ci.yml with correct placement in muxplex repo
- Matrix: Python 3.11, 3.12, 3.13 with fail-fast: false
- Uses uv for Python and dependency management with package caching
- Runs pytest via uv run pytest -v (config in pyproject.toml addopts)
- timeout-minutes: 15 to prevent runaway CI costs
- permissions: contents: read for minimal GITHUB_TOKEN scope
Co-authored-by: Amplifier <amplifier@example.com>
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.
- 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
- 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
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
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).
- 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
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.
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.
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).
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.
- 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
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).
- 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>
- 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.
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>
- 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
- 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)
- 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>
- 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
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.
- 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
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.
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.
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
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.