Commit Graph

155 Commits

Author SHA1 Message Date
Brian Krabach 70b8df1458 feat: sidebar device grouping when multiple sources configured (task-16)
- Replace renderSidebar to group sessions by deviceName when _sources.length > 1
- Render sidebar-device-header h4 elements before each device's sessions
- When single source configured, render flat list with no headers
- Click handlers now pass sourceUrl to openSession
- Add tests: groups with header when multiple sources, flat when single source

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-03-31 01:16:21 -07:00
Brian Krabach 88102751c8 style: fix code quality issues from review for task-15 remote instances UI
- Deduplicate CSS: merge shared properties of .settings-remote-url and
  .settings-remote-name into a combined selector, keeping only flex
  values in individual rules. Reduces 18 lines to 10.

- Fix remove button hover color: .settings-remote-remove:hover now uses
  #ef4444 (red) and rgba(239, 68, 68, 0.1) background, consistent with
  .tile-delete:hover and .sidebar-delete:hover for destructive actions.

- Add aria-label attributes to urlInput and nameInput in
  _buildRemoteInstanceRow for screen-reader accessibility:
  'Remote instance URL' and 'Remote instance display name'.

- Add 2 tests to verify aria-label attributes are set on both inputs
  (test_build_remote_instance_row_url_input_aria_label,
  test_build_remote_instance_row_name_input_aria_label).

All 615 tests pass.

Co-authored-by: Amplifier <amplifier@sourcegraph.com>
2026-03-31 01:09:27 -07:00
Brian Krabach c8a963f28e feat: add Remote Instances management UI to Settings (task-15) 2026-03-31 00:55:26 -07:00
Brian Krabach 200137a995 feat: add view mode and scope selectors to Display settings tab
Task: task-14 Settings dialog — view mode selector and scope toggle
2026-03-31 00:40:51 -07:00
Brian Krabach b2ff3613de feat: add loadGridViewMode and saveGridViewMode with local/server scope
- Add loadGridViewMode() that reads grid view mode preference based on
  viewPreferenceScope in display settings: returns gridViewMode from
  localStorage display settings when scope is 'local' (default), or
  grid_view_mode from _serverSettings when scope is 'server'. Returns
  'flat' as default when no preference is set.
- Add saveGridViewMode(mode) that saves to appropriate scope (localStorage
  via saveDisplaySettings when local, patchServerSetting when server)
  and updates _gridViewMode module variable.
- Wire loadGridViewMode() into DOMContentLoaded startup after applyDisplaySettings
  so _gridViewMode is initialized from stored preference on page load.
- Export both functions from module.exports for testing.

Closes task-13
2026-03-31 00:33:34 -07:00
Brian Krabach 393891173f fix: improve test assertion precision and add null guard in renderFilterBar
- Fixed imprecise pill count assertion in test_app.mjs by changing /filter-pill/g regex to /<button/g to accurately count button elements without double-counting when active pill has both classes
- Removed redundant OR fallback in active pill test, now relying only on specific regex for Laptop pill
- Added defensive null guard 'allSessions = allSessions || []' at entry of renderFilterBar in app.js to prevent TypeError

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-03-31 00:26:12 -07:00
Brian Krabach 5738c39f7f feat: add filtered view mode with device pill bar (task-12)
- Add <div id="filter-bar" class="filter-bar"> to index.html before session-grid div
- Implement renderFilterBar(container, allSessions) that renders pill buttons
  for each unique device name plus an 'All' button
- Active device pill gets filter-pill--active class
- Add _setActiveFilterDevice(device) test helper
- Bind delegated click handler on filter-bar in bindStaticEventListeners
  that sets _activeFilterDevice and re-renders grid
- Export renderFilterBar and _setActiveFilterDevice

Co-authored-by: Amplifier <amplifier@amplified.dev>
2026-03-31 00:16:15 -07:00
Brian Krabach 2e8cf6ddee feat: add grouped view mode to renderGrid with device-group-header sections
- Add renderGroupedGrid(sessions, mobile) that groups sessions by deviceName
  and creates <h3 class="device-group-header"> headers for each group
- Rewrite renderGrid to: apply device filter in 'filtered' mode, apply sort
  order, call renderGroupedGrid when _gridViewMode === 'grouped', render
  filter bar in 'filtered' mode, bind click handlers with sourceUrl
- Add _setGridViewMode(mode) test-only helper
- Export renderGroupedGrid and _setGridViewMode from module.exports
2026-03-31 00:00:34 -07:00
Brian Krabach 300919c7d7 feat: device badge on sidebar items in buildSidebarHTML (task-10) 2026-03-30 23:49:31 -07:00
Brian Krabach 4084544530 refactor(task-9): extract badgeHtml const and tighten device badge tests
- Extract inline ternary for device badge into `const badgeHtml` variable
  above the return block, consistent with how `bellHtml` is handled
- Tighten data-session-key/data-source-url test assertions to check exact
  attribute values rather than just attribute presence
- Add XSS test: verifies deviceName containing HTML is escaped in the badge

All 159 tests pass.
2026-03-30 23:41:50 -07:00
Brian Krabach 5980a6c080 feat: add device badge and data attributes to buildTileHTML (task-9)
- Show <span class="device-badge"> after tile name when _sources.length > 1
  and session.deviceName is present (multi-source federation context)
- Add data-session-key and data-source-url attributes to tile article
  element for unique session identification across sources
- Tests: 3 new tests covering badge visibility, badge omission for single
  source, and presence of new data attributes
2026-03-30 23:34:22 -07:00
Brian Krabach d897c05c61 feat: add device-badge, group-header, filter-bar, and sidebar-device-header CSS classes 2026-03-30 23:29:30 -07:00
Brian Krabach 4ed8303b6e feat: update getVisibleSessions to only hide local sessions by name
- Replace getVisibleSessions filter logic: only hide sessions where
  sourceUrl is empty/absent AND name matches hidden_sessions list
- Remote sessions with the same name as a hidden local session now
  remain visible (task-7 spec requirement)
- Export getVisibleSessions in module.exports so tests can access it

Tests added:
- 'getVisibleSessions exported and filters hidden sessions': verifies
  export and that local hidden sessions are filtered
- 'getVisibleSessions hides local sessions by name but not remote
  sessions with same name': verifies remote sessions survive

All 155 tests pass.
2026-03-30 23:22:55 -07:00
Brian Krabach 48e87af3f8 feat: wire buildSources into loadServerSettings startup
After fetching /api/settings and caching in _serverSettings,
call _sources = buildSources(_serverSettings) so the polling
layer immediately knows which sources to query.

Implements task-6 of multi-device federation phase 1.
2026-03-30 23:14:48 -07:00
Brian Krabach e95a8d4282 refactor: improve pollSessions error handling and test coverage 2026-03-30 23:09:08 -07:00
Brian Krabach e7ed300b37 feat: rewrite pollSessions for multi-source parallel polling (task-5) 2026-03-30 22:59:08 -07:00
Brian Krabach 593f1639db style: align tagSessions and mergeSources with ES6 file conventions 2026-03-30 22:52:36 -07:00
Brian Krabach 55f9597e1a feat: add tagSessions and mergeSources for multi-source parallel polling
These two functions enable multi-source session federation:
- tagSessions(sessions, deviceName, sourceUrl): tags each session with
  deviceName, sourceUrl, and sessionKey (format: sourceUrl::name),
  returns new objects without mutating originals
- mergeSources(results): takes [{source, sessions}] objects, tags each
  source's sessions and concatenates into a single flat array

Both functions are exported in module.exports.
6 new tests added covering all specified behaviors.
2026-03-30 22:42:55 -07:00
Brian Krabach 07c61a0baf style: expand test-only helper bodies to match section formatting 2026-03-30 22:37:08 -07:00
Brian Krabach 844311b475 feat: add federation state globals and test-only helpers to app.js
Add new module-level globals:
- let _gridViewMode = 'flat'
- let _activeFilterDevice = 'all'

Add test-only helper functions:
- _setSources(sources): sets internal _sources array
- _setServerSettings(settings): sets internal _serverSettings
- _getGridViewMode(): returns current _gridViewMode value
- _getSources(): returns current _sources array

All 4 helpers exported in module.exports.

Tests added (all passing, 143/143):
- '_setSources sets internal _sources array'
- '_setServerSettings sets internal _serverSettings'
- '_getGridViewMode returns current _gridViewMode value'
- '_getSources returns current _sources array'

Task: task-3 (federation state helpers)
2026-03-30 22:29:30 -07:00
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 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 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