Commit Graph

346 Commits

Author SHA1 Message Date
Brian Krabach ecf57441cf feat: add buildSources to construct session source list from settings
🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-03-30 22:21:45 -07:00
Brian Krabach 93c5170581 feat: refactor api() to accept optional baseUrl for cross-origin requests
Add 4th parameter `baseUrl` to `api()`. When provided, strips trailing
slash and prepends to path, and sets `credentials: 'include'` for
cross-origin cookie sending. Behavior is unchanged when no baseUrl is
given (backward compatible). Export `api` in module.exports.

All 135 tests pass.

🤖 Generated with Amplifier <https://github.com/microsoft/amplifier>

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-03-30 22:12:57 -07:00
Brian Krabach 3a102ba9d8 fix: add 18px font option, click-outside-dialog to dismiss settings
Added 18px to font size select. Dialog click handler checks
e.target === settingsDialog (click landed on ::backdrop area,
not dialog content) and closes settings.
2026-03-30 22:07:38 -07:00
Brian Krabach d0eb91dca7 refactor: replace fixed sleeps with deterministic polling in ws proxy tests; document CORS permissiveness 2026-03-30 21:56:05 -07:00
Brian Krabach b1cbeeb02c test: expand WebSocket proxy test suite 2026-03-30 21:47:17 -07:00
Brian Krabach 0e02181477 feat: add CORS middleware for cross-origin federation 2026-03-30 21:47:17 -07:00
Brian Krabach ca0eb44b88 feat: add GET /api/instance-info endpoint
Implement new public endpoint that returns device name and version without authentication required. The auth exemption was already in place from task 4.
2026-03-30 21:47:17 -07:00
Brian Krabach e61efd141c feat: exempt /api/instance-info from auth 2026-03-30 20:54:21 -07:00
Brian Krabach 395278818f feat: add remote_instances and device_name to settings
- Add 'remote_instances': [] to DEFAULT_SETTINGS for peer instance configs
- Add 'device_name': '' to DEFAULT_SETTINGS (empty for clean JSON serialization)
- Add import socket to settings.py
- In load_settings(), fill empty device_name with socket.gethostname() so the
  hostname is always current even if the machine is renamed
- Add 7 new tests covering defaults, hostname fallback, round-trips, and
  mutation isolation
- Update 2 existing tests that compared result == DEFAULT_SETTINGS to account
  for the hostname fill-in behavior

Co-authored-by: Amplifier <amplifier@amplified.dev>
2026-03-30 20:48:38 -07:00
Brian Krabach e94a27d2f8 chore: remove stale requirements.txt 2026-03-30 20:42:57 -07:00
Brian Krabach c049ad5628 chore: remove dead Caddyfile 2026-03-30 20:39:34 -07:00
Brian Krabach a5b02b74d2 Add multi-device federation design document
Covers:
- Backend settings & remote instance registry
- Frontend multi-origin API layer with parallel polling
- View modes (flat/grouped/filtered) & device tagging
- Remote auth flow (per-instance browser auth)
- Terminal connection for remote sessions (direct WebSocket)
- Unreachable instance handling with exponential backoff
- Cleanup: remove Caddyfile, requirements.txt, expand WS proxy tests
2026-03-30 20:04:13 -07:00
Brian Krabach 70da8dad38 fix: don't clear bells when browser tab is hidden
sendHeartbeat() always reported _viewingSession even when the user
switched to another browser tab. The server saw 'device is watching
session X' and cleared bells immediately. Fix: use Page Visibility API
(document.hidden) to send viewing_session=null when the tab is in the
background. Bells accumulate correctly, favicon badge shows activity,
and indicators appear in the dashboard/sidebar. When the user switches
back to the muxplex tab, the next heartbeat restores the real session.
2026-03-30 19:03:27 -07:00
Brian Krabach 79576bcbfe feat: customizable delete session command template
Symmetric with create template. Default: 'tmux kill-session -t {name}'.
Configurable in Settings → Commands tab (renamed from 'New Session').
DELETE /api/sessions/{name} reads template from settings, substitutes
{name}, runs synchronously via subprocess.run with 30s timeout.

Enables custom teardown workflows like:
  amplifier-dev ~/dev/{name} --destroy

Changes:
- settings.py: add delete_session_template to DEFAULT_SETTINGS
- main.py: DELETE endpoint uses subprocess.run + template instead of run_tmux
- index.html: add delete template textarea + reset btn; rename tab to Commands
- app.js: DELETE_SESSION_DEFAULT_TEMPLATE constant, openSettings loads it,
  bindStaticEventListeners wires input/reset; constant exported

Tests added:
- test_settings.py: 3 tests for default, load, and patch of delete_session_template
- test_api.py: 2 tests for custom/default template substitution via subprocess mock;
  update existing test_delete_session_calls_kill_session to match new implementation
- test_frontend_html.py: 5 tests for textarea, placeholder, rows, reset btn, tab label
- test_app.mjs: 5 tests for constant definition, value, and wiring in openSettings/bindStaticEventListeners
2026-03-30 17:17:34 -07:00
Brian Krabach 2a30e88feb fix: launchd plist includes PATH with Homebrew + user local bin
launchd agents run with minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin).
Homebrew binaries (tmux, ttyd) in /opt/homebrew/bin and uv tool installs
in ~/.local/bin were not found, causing _check_dependencies() to exit(1).
Plist now includes EnvironmentVariables with a PATH that covers both
Apple Silicon and Intel Homebrew locations + ~/.local/bin.
2026-03-30 16:39:02 -07:00
Brian Krabach 320c9f3ba7 fix: macOS launchd service — modern bootstrap API, auto-start, --host 0.0.0.0
Replace deprecated launchctl load/unload with bootstrap/bootout (modern
macOS API). Auto-bootout existing service before reinstall (fixes 'I/O
error' on reload). Add --host 0.0.0.0 to plist ProgramArguments so the
service is actually accessible from the network. install-service now
auto-starts the service and prints management commands. upgrade() also
uses modern API. doctor() checks if service is actually running.
2026-03-30 08:52:43 -07:00
Brian Krabach b53d1abbd2 feat: smart version check in upgrade + doctor
Detect install source via PEP 610 direct_url.json:
- git+https: compare installed commit sha vs git ls-remote HEAD
- PyPI: compare installed version vs pypi.org/pypi/muxplex/json
- editable: skip (user manages source directly)
- unknown: upgrade to be safe

upgrade() skips reinstall if already up to date. --force overrides.
doctor() shows install source, commit hash, and update availability.
2026-03-30 08:33:24 -07:00
Brian Krabach 85798783b6 feat: add muxplex upgrade/update — stop service, reinstall, regenerate, restart
Platform-aware: launchctl on macOS, systemd on Linux/WSL.
Prefers uv tool install, falls back to pip.
Regenerates service file (picks up any plist/unit changes).
Runs doctor at the end for verification.
'update' is an alias for 'upgrade'.
2026-03-30 08:19:08 -07:00
Brian Krabach b448f120c5 feat: dynamic favicon badge — amber dot overlay when sessions have unseen bells 2026-03-30 08:06:34 -07:00
Brian Krabach 261a25e6df feat: inject hostname into page title — visible in browser tabs across machines 2026-03-30 08:05:14 -07:00
Brian Krabach df3e252f73 fix: exempt static assets from auth middleware — login page can load CSS/JS/images 2026-03-30 08:03:49 -07:00
Brian Krabach e6add00f0d feat: loading placeholder tile while new session is being created
- Inject .tile--loading skeleton tile into #session-grid immediately after
  POST /api/sessions succeeds, giving instant visual feedback while the
  backend creates the tmux session (which may take several seconds)
- Remove the placeholder when the poll finds the session or on timeout
- Add shimmer animation via @keyframes shimmer to style.css
- Tile placed after original .tile-body pre rule to avoid selector-substring
  match in _extract_rule_block CSS tests
2026-03-30 07:39:18 -07:00
Brian Krabach b38c87185a fix: settings dialog always visible — display:flex overrode native <dialog> hidden state
<dialog> is hidden by default via UA stylesheet (display:none). Our CSS
set display:flex unconditionally, overriding the UA rule and making the
dialog always visible. Fix: only apply display:flex when the dialog has
the [open] attribute (added by showModal()).
2026-03-30 07:14:24 -07:00
Brian Krabach 74a02cc05b fix: add six dependency — undeclared transitive dep of python-pam (breaks macOS clean envs)
python-pam imports six but doesn't declare it. On Linux, six is usually
present system-wide. On macOS uv tool environments (clean/isolated),
import pam fails with 'No module named six'. Adding six explicitly to
our deps ensures PAM auth works on macOS.
2026-03-30 07:06:08 -07:00
Brian Krabach 514ed5dc77 fix: add python-multipart dependency, fix launchd service name
python-multipart is required by FastAPI for form POST handling (login).
Was missing from pyproject.toml — crash on macOS first run.

launchd plist now uses the muxplex entry point script directly instead
of python -m muxplex, so macOS shows 'muxplex' in Activity Monitor,
launchctl list, and login items instead of 'python3'.
2026-03-30 07:01:01 -07:00
Brian Krabach cf1182b100 feat: add muxplex doctor subcommand — dependency + config diagnostics
Checks Python version, tmux, ttyd, muxplex version, settings file,
auth status (PAM/password/auto-generate), active tmux sessions,
platform, and service installation status. Platform-specific install
hints for missing dependencies. Non-fatal — always completes.
2026-03-30 06:47:36 -07:00
Brian Krabach d20f85a23e docs: add prerequisites section to README with platform-specific install instructions
- Expand Prerequisites section with bullet format (was a sparse table)
- Add tmux and ttyd per-platform install commands (macOS, Ubuntu/WSL)
- Note that muxplex prints install instructions if either dependency is missing
- Update 'Install as a Service' section with macOS launchd instructions
- Update Usage table to reflect platform-aware install-service behavior
2026-03-30 06:30:55 -07:00
Brian Krabach 54ad5fbeea feat: preflight dependency check for tmux and ttyd at startup
- Add _check_dependencies() that checks shutil.which() for tmux and ttyd
- Print clear error with per-platform install hints when either is missing
- Call _check_dependencies() in main() before serve — not for install-service,
  show-password, or reset-secret (those don't need the binary dependencies)
- Tests: exits on missing ttyd, exits on missing tmux, passes when both present,
  called for serve but not for install-service
2026-03-30 06:30:45 -07:00
Brian Krabach eb8b09a3cf feat: platform-aware install-service — launchd on macOS, systemd on Linux
- Add _install_launchd() that writes ~/Library/LaunchAgents/com.muxplex.plist
- Refactor existing systemd code into _install_systemd()
- install_service() now detects sys.platform == 'darwin' and delegates accordingly
- Update install-service help text to mention both platforms
- Tests: launchd plist on macOS, systemd on Linux, no cross-contamination
2026-03-30 06:29:45 -07:00
Brian Krabach d1c245a2c3 test: remove 26 stale command palette tests (palette was removed in Phase 1) 2026-03-30 03:45:42 -07:00
Brian Krabach 17616166bc feat: kill session — DELETE endpoint, hover × button with confirmation 2026-03-30 03:35:39 -07:00
Brian Krabach ffa8167c8f fix: new session poll-until-ready before auto-opening 2026-03-30 03:30:16 -07:00
Brian Krabach 424a4f5377 fix: settings modal dismiss — add close button, fix backdrop click 2026-03-30 03:27:47 -07:00
Brian Krabach 6f598524a9 fix: apply ruff formatting and add null guard for tab lookup in test
- Apply ruff auto-format to test_frontend_css.py, test_frontend_html.py,
  test_frontend_js.py (whitespace/line-length normalization)
- Add assert-not-None guard before tab.get() in settings tabs test,
  matching the pattern already used for display_tab (fixes pyright error)

Co-authored-by: Amplifier <amplifier@amplified.dev>
2026-03-30 02:54:07 -07:00
Brian Krabach bac23f26dc fix: apply getVisibleSessions filter in renderSheetList for mobile bottom sheet
Hidden sessions were still appearing in the mobile bottom-sheet session
switcher because renderSheetList() read _currentSessions directly,
bypassing the getVisibleSessions() helper used by renderGrid() and
renderSidebar().

- app.js: sortByPriority now receives getVisibleSessions(_currentSessions)
  instead of _currentSessions directly in renderSheetList()
- test_frontend_js.py: add test_render_sheet_list_filters_hidden_sessions
  to assert getVisibleSessions is called in renderSheetList body

Completes consistent hidden-sessions filtering across all three render
paths (grid, sidebar, mobile bottom sheet).

Co-authored-by: Amplifier <amplifier@amplified.dev>
2026-03-30 02:49:41 -07:00
Brian Krabach d2f3b43759 test: add frontend tests for settings dialog and new session UI 2026-03-30 02:36:08 -07:00
Brian Krabach 851036bac0 refactor: code quality improvements for task-7 settings effects
- Extract getVisibleSessions() helper to consolidate hidden-session
  filter used by renderGrid() and renderSidebar() (DRY reduction)
- Add comment to terminal.js magic literal 600 noting it matches
  MOBILE_THRESHOLD in app.js
- Move TERMINAL_JS_PATH and _TERMINAL_JS module fixtures to the top
  of the test file alongside JS_PATH and _JS for consistency
- Strengthen test_render_grid_uses_visible_for_empty_state_check to
  assert visible.length specifically, distinguishing it from
  test_render_grid_creates_visible_array
- Update renderGrid/renderSidebar filter tests to assert getVisibleSessions()
  call; add test_get_visible_sessions_filters_hidden_sessions to cover
  the extracted helper directly
2026-03-30 02:28:54 -07:00
Brian Krabach 142c68d644 feat: apply settings effects — font size, grid columns, hidden sessions, sort order
- terminal.js createTerminal(): read fontSize from localStorage 'muxplex.display',
  default 14, apply Math.min(storedFontSize, 12) mobile cap
- app.js renderGrid(): filter hidden sessions via _serverSettings.hidden_sessions,
  create visible array, apply alphabetical sort via localeCompare when sort_order is
  'alphabetical'; 'recent'/'manual' use server-provided order
- app.js renderSidebar(): apply same hidden sessions filter, use visible array
- app.js DOMContentLoaded: call loadServerSettings() after startPolling() in restoreState().then()
- tests: 13 new tests in test_frontend_js.py covering all spec requirements

Co-authored-by: Amplifier <amplifier@example.com>
2026-03-30 02:16:45 -07:00
Brian Krabach 1af141a294 docs: fix orphaned JSDoc — move showNewSessionInput comment above its function 2026-03-30 02:07:15 -07:00
Brian Krabach 5b971a0ecc refactor: apply code quality suggestions from task-6-mobile-fab review
Three suggestions from the code review applied:

1. Extract _createSessionInput() factory to eliminate five-line
   duplication between showNewSessionInput and showFabSessionInput.
   Both functions now call the shared factory. Updated 4 existing
   tests to check _createSessionInput's body; added 7 new tests
   covering factory behavior and factory usage in each consumer.

2. Fix .new-session-fab:focus-visible outline color from var(--accent)
   to var(--bg) — the FAB background is already var(--accent), so
   using the same color for the outline created zero contrast.
   Added 1 new test to guard this going forward.

3. Add early-return guard in showFabSessionInput against duplicate
   overlays: if (document.querySelector('.fab-input-overlay')) return;
   Prevents a second overlay being created if called programmatically
   while one is already open. Added 1 new test.

532 tests pass (9 new tests added, 523 unchanged).
2026-03-30 02:00:22 -07:00
Brian Krabach 9978544dc1 fix: use fixed-position overlay for FAB session input (mobile visibility) 2026-03-30 01:45:51 -07:00
Brian Krabach dbb8b2bd73 feat: add mobile FAB for new session creation 2026-03-30 01:34:31 -07:00
Brian Krabach d76042560a feat: add sidebar + New sticky footer with inline input
- Add div.sidebar-footer with button#sidebar-new-session-btn.sidebar-new-btn
  ('+ New') to #session-sidebar in index.html, positioned after #sidebar-list
- Add CSS for .sidebar-footer (padding: 8px, border-top, flex-shrink: 0)
- Add CSS for .sidebar-new-btn (width: 100%, dashed border, dim text, hover
  effects with muted text and border color)
- Bind #sidebar-new-session-btn click to showNewSessionInput() in
  bindStaticEventListeners()
- Add 15 tests covering HTML structure, CSS rules, and JS binding

🤖 Co-authored-by: Amplifier <amplifier@example.com>
2026-03-30 01:23:37 -07:00
Brian Krabach 84fb7826d7 feat: add header + button with inline name input for session creation
- Add showNewSessionInput(btn): creates inline text input with class
  'new-session-input', placeholder 'Session name…', autocomplete off,
  spellcheck false; hides button, inserts input before it, focuses it.
  Handles Enter (create), Escape (cancel), and blur (150ms delayed cleanup).
- Add createNewSession(name): POSTs to /api/sessions, shows toast with
  created session name, calls pollSessions(), and if ss.auto_open_created
  !== false calls openSession(data.name) after 500ms delay.
- Bind #new-session-btn click to showNewSessionInput in bindStaticEventListeners.
- Export showNewSessionInput and createNewSession from module.exports.
- Add .new-session-input CSS (bg, border 1px solid accent, border-radius 4px,
  font-size 13px, font-ui, padding 4px 10px, width 180px, outline none) and
  ::placeholder { color: var(--text-dim) } in style.css.
- Add 28 new tests covering all spec requirements (19 JS structural, 9 CSS).
2026-03-30 01:08:19 -07:00
Brian Krabach 5f81b87348 feat: add New Session settings tab with template textarea 2026-03-30 01:03:54 -07:00
Brian Krabach 2e86692c3c fix: add null guard to _updateNotificationUI and .catch() to requestPermission
- _updateNotificationUI: add guard 'if (!statusEl || !reqBtn) return;' so
  future call sites without explicit null checks cannot throw TypeError
- Notification.requestPermission(): add .catch(console.error) for defensive
  error handling — promise always resolves in practice but unhandled rejections
  would be silently swallowed
- Tests: add test_update_notification_ui_has_null_guard and
  test_bind_static_event_listeners_permission_btn_has_catch to enforce both

435 tests pass (2 new)

Co-authored-by: Amplifier <amplifier@amp.dev>
2026-03-30 00:35:21 -07:00
Brian Krabach e5827a8037 refactor: extract _updateNotificationUI helper, use const, fix double query
- Extract shared _updateNotificationUI(statusEl, reqBtn, permission) helper
  to remove duplicated notification status UI update logic from openSettings
  and the requestPermission click handler
- Replace var with const for local bindings in new Notifications handlers
- Use this.checked in bell sound change handler instead of re-querying
  the DOM element that was already looked up for addEventListener
2026-03-30 00:29:38 -07:00
Brian Krabach 2f909eae89 feat: add Notifications settings tab 2026-03-30 00:18:05 -07:00
Brian Krabach c955aadad1 feat: add Sessions settings tab with server-side persistence 2026-03-29 23:57:22 -07:00
Brian Krabach f380307f08 fix: rename data-panel to data-tab on settings panels to match switchSettingsTab()
switchSettingsTab() in app.js reads panel.dataset.tab (data-tab attribute).
The four .settings-panel elements in index.html were using data-panel instead,
causing all panels to get hidden on every tab click — the settings dialog rendered
as a blank content area after any tab interaction.

Fix: Change data-panel="..." to data-tab="..." on all four panel divs.

Tests added:
- test_html_settings_panels_use_data_tab: verifies each .settings-panel uses
  data-tab and not data-panel
- test_html_settings_tab_panel_data_tab_alignment: cross-checks that every tab
  button data-tab value has a matching .settings-panel data-tab value, preventing
  this class of HTML/JS attribute mismatch from recurring
2026-03-29 23:47:59 -07:00