Commit Graph

420 Commits

Author SHA1 Message Date
Brian Krabach 80a04cb2cc style: ruff format test_readme.py 2026-03-31 17:39:59 -07:00
Brian Krabach fc9376a69a docs: add service subcommand documentation to README 2026-03-31 17:34:56 -07:00
Brian Krabach e1dfe8e10c refactor: remove old install_service/launchd/systemd from cli.py, replaced by service module 2026-03-31 17:24:34 -07:00
Brian Krabach 496c37a32f refactor: install-service deprecated alias now forwards to service_install() 2026-03-31 17:05:43 -07:00
Brian Krabach ff2abb5f77 feat: wire service subcommand group into CLI (install, uninstall, start, stop, restart, status, logs) 2026-03-31 16:58:08 -07:00
Brian Krabach 8637b740c0 fix: launchd quality — use _LAUNCHD_LABEL in plist template, add restart/status tests 2026-03-31 16:53:17 -07:00
Brian Krabach fbfbab8bb8 feat: implement launchd service commands (install, uninstall, start, stop, restart, status, logs) 2026-03-31 16:46:31 -07:00
Brian Krabach 376583bf23 feat: implement systemd service commands (install, uninstall, start, stop, restart, status, logs) 2026-03-31 16:38:46 -07:00
Brian Krabach f360dea767 feat: create service.py module skeleton with platform dispatch 2026-03-31 16:21:49 -07:00
Brian Krabach 103944e318 fix: remove redundant sys import and update upgrade() service install references
- Remove inner 'import sys' inside install-service branch (sys already
  imported at module level on line 8; was triggering PLC0415 smell)
- Update two fallback messages in upgrade() from 'muxplex install-service'
  to 'muxplex service install' — consistent with deprecation warning and
  doctor() messages updated in Task 5
2026-03-31 15:31:47 -07:00
Brian Krabach 1448b70696 style: ruff format test_settings.py 2026-03-31 15:23:41 -07:00
Brian Krabach a1e532cb3f style: reorder devices panel — enable checkbox before device name input
The multi-device enable checkbox is the gating control; device name
only matters when multi-device is on. Place the checkbox first to
reinforce the logical dependency between the two settings.

No behavioral change — no tests enforce element ordering within a panel.
2026-03-31 15:20:32 -07:00
Brian Krabach ae07431ed1 fix: move notifications/device-name elements to correct settings panels
- Move #setting-bell-sound and notification controls from sessions panel
  to a new notifications panel (data-tab="notifications")
- Move #setting-device-name from display panel to devices panel
- Add #setting-view-scope select to devices panel

Fixes 8 test failures in test_frontend_html.py:
- test_html_settings_panels_use_data_tab (now 5 panels)
- test_html_notifications_panel_has_bell_sound_checkbox
- test_html_notifications_panel_has_notification_status_text
- test_html_notifications_panel_has_request_btn
- test_html_sessions_tab_device_name_input
- test_html_devices_panel_has_device_name
- test_html_devices_panel_has_view_scope
2026-03-31 15:15:29 -07:00
Brian Krabach ba1f28b65a docs: update README Usage section for config-driven serve behavior 2026-03-31 15:04:14 -07:00
Brian Krabach 8d465ad8a5 fix: test suite green after Phase 1 config refactor 2026-03-31 15:04:14 -07:00
Brian Krabach 22b85012eb feat: update doctor() to show serve config and fix service install message
- Add serve config display in doctor() showing host, port, auth, and
  session_ttl from settings.json (inserted after Settings file block)
- Update two 'not installed' messages from 'muxplex install-service'
  to 'muxplex service install' (macOS launchd and Linux systemd sections)
- Add test_doctor_shows_serve_config verifying custom settings values
  (0.0.0.0, 9999, password) appear in doctor() output
2026-03-31 15:04:14 -07:00
Brian Krabach 8bb7bcbb67 test: add deprecation warning test for install-service subcommand
- Add test_install_service_subcommand_prints_deprecation_warning to
  verify that 'muxplex install-service' prints a deprecation warning
  to stderr containing 'deprecated' and 'muxplex service install'
- Update the deprecation warning message in main() to include
  'muxplex service install' as the recommended replacement command

Refs: task-4
2026-03-31 15:04:14 -07:00
Brian Krabach 03e9dc66de refactor: argparse None defaults, serve flags on both parsers, upgrade alias
- Add _add_serve_flags() helper to set --host/--port/--auth/--session-ttl
  all with default=None so serve() can distinguish 'not passed' from
  'passed the default value'
- Apply _add_serve_flags() to both root parser and 'serve' subparser so
  'muxplex serve --host X --port Y' is accepted
- Consolidate upgrade/update via aliases=['update'] instead of two
  separate subparsers — help now shows 'upgrade (update)'
- Print deprecation warning to stderr when 'install-service' is used
- Update 5 existing tests to expect None instead of hardcoded defaults
- Add 4 new tests: test_main_passes_none_for_unset_flags,
  test_main_passes_explicit_host_only,
  test_main_serve_subcommand_accepts_flags,
  test_help_shows_single_upgrade_line

All 55 tests pass.
2026-03-31 15:04:14 -07:00
Brian Krabach c057ad6325 feat: refactor serve() to read settings.json with CLI override sentinel (task-2)
- Change serve() signature: all params now default to None (sentinel for 'not passed by CLI')
- Add load_settings() call: resolution order is CLI flag > settings.json > hardcoded default
- Switch from os.environ.setdefault() to os.environ[] hard assignment so settings.json values take effect
- Add 6 new tests: host/port/session_ttl from settings, CLI override, fallback to defaults, session_ttl=0 as valid value
2026-03-31 15:04:14 -07:00
Brian Krabach a4c2a8e3b8 feat: add serve keys (host, port, auth, session_ttl) to DEFAULT_SETTINGS
Add four new server configuration keys to DEFAULT_SETTINGS in settings.py:
- host: '127.0.0.1'
- port: 8088
- auth: 'pam'
- session_ttl: 604800

Serve keys are placed first in the dict as primary server configuration.
Existing keys (default_session, sort_order, etc.) follow unchanged.

Add four tests covering the new keys:
- test_default_settings_include_serve_keys
- test_load_settings_returns_serve_keys_when_file_missing
- test_serve_keys_patchable
- test_old_settings_file_without_serve_keys_loads_correctly
2026-03-31 15:04:14 -07:00
Brian Krabach c745660afe docs: CLI & service management refactor design
- Config file (settings.json) becomes source of truth for serve options
- Replace install-service with muxplex service <command> subgroup
- Clean up CLI structure (upgrade/update alias, serve flag precedence)
- Thin wrappers over systemctl/launchctl for install/uninstall/start/stop/restart/status/logs
- Backward compat: install-service remains as deprecated alias
2026-03-31 15:01:49 -07:00
Brian Krabach 3de9e98de8 refactor: reorganize settings — merge Notifications into Sessions, move device name to Display, remove view scope 2026-03-31 11:53:25 -07:00
Brian Krabach 3707283baf fix: resolve merge test regressions — openSession connect POST and cycleViewMode mock 2026-03-31 09:21:05 -07:00
Brian Krabach 5a67a3eb3e merge: integrate latest upstream (fit view refactor, ttyd kill-by-port, mobile viewport fix) 2026-03-31 09:10:59 -07:00
Brian Krabach b450cf7176 refactor: fit view — pure CSS layout, no DOM measurement in applyFitLayout
Root cause of repeated fit view failures: applyFitLayout() measured
clientHeight/clientWidth/getComputedStyle and set inline tile heights. This
failed in multiple ways:
  - clientHeight = 0 when container is display:none (returning from session)
  - inline tile.style.height destroyed every 2s by innerHTML rebuild in pollSessions
  - getComputedStyle forces style recalc with potentially stale values
  - rAF wrappers couldn't reliably fix timing issues

Fix: pure arithmetic applyFitLayout — no DOM measurement, no inline heights.
The grid already has a definite height from CSS (flex:1 inside height:100dvh).
grid-template-rows: repeat(rows, 1fr) divides that space without JS measurement.

CSS changes:
  - .session-grid--fit { align-content: stretch }
  - .session-grid--fit .session-tile { height: auto } — CSS grid cell controls sizing

JS changes:
  - applyFitLayout() removes all clientHeight/clientWidth/getComputedStyle/style.height
  - applyFitLayout() is now pure arithmetic: count tiles, compute cols×rows, set 1fr templates
  - Removed rAF wrappers from all call sites (renderGrid, closeSession, resize handler,
    applyDisplaySettings) — safe to call synchronously since no layout measurement needed
  - applyDisplaySettings() no longer clears tile heights (never set them anymore)

Tests updated:
  - Added test_fit_view_session_tile_has_height_auto (CSS)
  - Added 'applyFitLayout does NOT measure DOM dimensions' (JS)
  - Updated 'applyFitLayout clears stale...' → 'sets gridTemplateColumns/Rows via arithmetic'
  - Updated rAF-wrapper test to match new direct-call behavior
2026-03-31 07:47:54 -07:00
Brian Krabach 9762098a00 fix: session switching — kill ttyd by port not just PID file (belt-and-suspenders)
kill_ttyd() now uses two strategies:
1. PID file (existing behavior — kept intact)
2. Port-based fallback: lsof -ti :{TTYD_PORT} finds and kills any orphaned
   ttyd process that wasn't tracked in the PID file

spawn_ttyd() also does a pre-spawn port-free guard: if any process is still
occupying TTYD_PORT after kill_ttyd() returns, it sends SIGKILL to force-free
the port before the new ttyd tries to bind.

Root cause: PID file became desynced (pointed to dead process). kill_ttyd()
thought it succeeded but the REAL ttyd (PID not in file) kept running on
port 7682. New spawn_ttyd() failed to bind, died silently. Old ttyd kept
serving the old session — so every session switch showed the same session.

Tests: 2 new tests in test_ttyd.py (RED → GREEN confirmed)
2026-03-31 07:47:30 -07:00
Brian Krabach 0ed03c4e9d fix: fit view — clear stale layout on recalc, revert to position:absolute bottom:0
Bug 1: applyFitLayout now clears grid-template-rows and tile heights
before recalculating. Prevents stale layout from empty-grid calls on
page reload from interfering with subsequent calculations.

Bug 2: Removed flex justify-content:flex-end approach — didn't work
because pre filled 100% of parent (flex-end had no effect). Reverted
to base CSS position:absolute + bottom:0 which anchors content to the
bottom. With 80 lines in fit mode, content always overflows the tile
and excess is clipped at the top by tile-body overflow:hidden.
2026-03-31 06:55:05 -07:00
Brian Krabach 4e78f6f5ac fix: restore default title when device name cleared, fix stale comments, set title at load 2026-03-31 06:36:49 -07:00
Brian Krabach bee74f5c1b fix: add style property to openSettings test mocks for multi-device field state 2026-03-31 06:28:55 -07:00
Brian Krabach 679416a5bf feat: reorganize settings dialog — add Multi-Device tab, enable/disable toggle, device name as page title
- Add multi_device_enabled: false to DEFAULT_SETTINGS in settings.py
- Add fifth 'Multi-Device' tab (data-tab='devices') to settings dialog sidebar
- Move #setting-device-name, #setting-remote-instances, #add-remote-instance-btn,
  #setting-view-mode, and #setting-view-scope out of Display/Sessions tabs into
  the new dedicated devices panel
- Add #setting-multi-device-enabled checkbox at top of Multi-Device tab;
  when unchecked, all other fields in the tab are greyed out and disabled
- Gate buildSources() remote instances behind multi_device_enabled (smart default:
  treated as enabled if remote_instances is non-empty for backward compat)
- Wire device name → document.title: updates title on openSettings() and on
  every keystroke in the device name input field
- Add _updateMultiDeviceFieldsState() helper to enable/disable fields
- Add #multi-device-fields CSS transition for smooth opacity animation
- Add 19 new tests (settings, HTML, JS, CSS) covering all new behavior
2026-03-31 06:16:44 -07:00
Brian Krabach b282e42ae8 fix: mobile viewport cutoff (100dvh) + fit view content anchoring (flex-end)
Mobile: 100vh includes browser chrome on mobile — bottom row clipped.
Fix: 100dvh with 100vh fallback (progressive enhancement) in .view
and .session-tile--expanded.

Fit view: scrollTop hack reset every 2s by poll cycle and had no effect
when content didn't overflow. Fix: replace position:absolute+scrollTop
with flex+justify-content:flex-end on tile-body. Content naturally
anchors to bottom without JS. No scrollbar hiding needed.

Tests: remove test_fit_view_pre_has_top_zero (old behavior), add
test_view_uses_dvh_fallback, test_fit_view_tile_body_uses_flex_end,
test_fit_view_pre_is_static. Update JS test for new flex approach.
2026-03-31 05:54:41 -07:00
Brian Krabach 881b6cc9f3 merge: integrate upstream changes with multi-device federation 2026-03-31 05:48:49 -07:00
Brian Krabach c330f340ff fix: renderGrid shows status tiles when no sessions; _previewClickHandler forwards sourceUrl
- renderGrid early-return now builds status tiles for auth_required and
  unreachable sources even when visible.length === 0. Hides empty-state
  when status tiles are present, shows it only when truly nothing to display.

- _previewClickHandler now looks up the session in _currentSessions to
  recover its sourceUrl before calling openSession, ensuring remote
  sessions opened via hover-preview connect to the correct instance
  instead of falling back to the local API path.

Tests added (3 new, 214 → 217 total):
- renderGrid shows auth tile and hides empty-state when no sessions but source is auth_required
- renderGrid shows offline tile and hides empty-state when no sessions but source is unreachable
- _previewClickHandler looks up sourceUrl from _currentSessions before calling openSession

Closes code-review Important issues #1 and #2.

Co-authored-by: Amplifier <amplifier@anthropic.com>
2026-03-31 04:29:49 -07:00
Brian Krabach 9befe9997e test: add formatLastSeen verification tests across all time ranges 2026-03-31 04:09:43 -07:00
Brian Krabach ddc52f2b36 feat: grid click handler routes sourceUrl via data-source-url attribute on tiles
- In buildTileHTML, make data-source-url attribute conditional: only add when
  session.sourceUrl is non-empty (local sessions omit the attribute entirely)
- In renderGrid click/keydown handlers, pass sourceUrl with || '' fallback so
  local sessions (where data-source-url is absent) pass empty string to openSession
- Add 2 tests: one verifying data-source-url present for remote sessions,
  one verifying data-source-url absent for local sessions (empty sourceUrl)

Task: task-9-grid-click-routes-source-url
2026-03-31 04:01:56 -07:00
Brian Krabach 28d32d21be feat: closeSession skips DELETE for remote sessions
- Guard DELETE /api/sessions/current with if (!_viewingSourceUrl) check
- Reset _viewingSourceUrl to '' after the conditional DELETE
- For remote sessions, the remote instance doesn't need to know we
  stopped watching - just disconnect WebSocket and update local UI

Tests added:
- closeSession does NOT fire DELETE for remote session (non-empty _viewingSourceUrl)
- closeSession still fires DELETE /api/sessions/current for local session

Task: task-8-close-session-remote-aware
2026-03-31 03:53:34 -07:00
Brian Krabach 0bb66b1801 fix: fit view — reapply layout on session close, show more lines, bottom-anchor content
Bug 1: closeSession() didn't call applyFitLayout when returning to
dashboard — tiles kept old heights from before the overview was hidden.
Fix: add rAF call after overview.style.display = '' so the layout is
remeasured once the overview becomes visible again.

Bug 2: Only 20 lines shown in tiles — not enough for tall fit-mode
tiles, and content sat at the top. Fix:
  - buildTileHTML shows 80 lines in fit mode (vs 20 in auto)
  - renderGrid rAF sets pre.scrollTop = pre.scrollHeight after
    applyFitLayout so content anchors at the bottom (like a real
    terminal). Hidden scrollbar via scrollbar-width:none + ::-webkit.

Tests: 3 new source-scan tests added and verified RED before GREEN.
2026-03-31 03:41:28 -07:00
Brian Krabach ac0b84c1d9 feat: openSession — full remote routing via sourceUrl 2026-03-31 03:40:57 -07:00
Amplifier 3444310bb2 feat: accept sourceUrl parameter in connectWebSocket for remote sessions (task-6)
🤖 Generated with Amplifier
2026-03-31 03:27:30 -07:00
Brian Krabach d955c90c0b feat: add openLoginPopup function and delegated click handler for auth tile login buttons
- Add openLoginPopup(remoteUrl) function after buildOfflineTileHTML that strips
  trailing slashes from remoteUrl before appending /login and opens a popup
  window with width=500,height=600
- Wire up delegated click handler for .source-tile__login-btn buttons inside
  bindStaticEventListeners, right after the existing delegated kill-session handler
- Export openLoginPopup from module.exports

Tests: 4 new tests covering openLoginPopup export, window.open call
with correct URL and dimensions, trailing slash handling (with and without)
2026-03-31 03:20:21 -07:00
Brian Krabach ebec7c3625 fix: fit view — defer layout measurement to rAF, stretch pre to fill tile
Bug 1: applyFitLayout measured parentElement.clientHeight before the
browser reflowed the DOM — returned 0 on first render. Fix: wrap in
requestAnimationFrame so measurement happens after layout pass.
Applies to all three call sites: renderGrid(), applyDisplaySettings(),
and the window resize handler.

Bug 2: .tile-body pre had position:absolute bottom:0 but no top:0.
In fit mode (taller tiles), content sat at the bottom with a gap above.
Fix: .session-grid--fit .tile-body pre { top: 0; overflow-y: auto }
2026-03-31 03:19:37 -07:00
Brian Krabach 2862f494f5 feat: integrate source status tiles into renderGrid
- Append auth tiles for auth_required sources after session tiles
- Append offline tiles for unreachable sources after session tiles
- _setSources test helper was already defined and exported (verified)
- All 197 tests pass (195 pre-existing + 2 new)
2026-03-31 03:11:47 -07:00
Brian Krabach bf87f05d82 test: add formatLastSeen branch coverage for seconds, hours, and days 2026-03-31 03:06:29 -07:00
Brian Krabach fe5337840d refactor: remove Compact view mode — keep Auto + Fit only 2026-03-31 03:05:18 -07:00
Brian Krabach e07cf196c6 feat: add formatLastSeen and buildOfflineTileHTML functions
- Add formatLastSeen(ms) to format millisecond timestamps into relative
  strings (Xs ago, Xm ago, Xh ago, Xd ago, Never for null)
- Add buildOfflineTileHTML(source) to render unreachable device tiles
  with Offline badge and last-seen timestamp
- Both functions added after buildAuthTileHTML in app.js
- Both exported via module.exports
- 7 new tests covering export, CSS class, device name, badge, last-seen,
  HTML escaping, and null handling
2026-03-31 02:45:36 -07:00
Brian Krabach 83e5318ba1 feat: add buildAuthTileHTML for auth-required federation tiles
- Add buildAuthTileHTML(source) function to frontend/app.js after buildSidebarHTML
- Function renders an <article class='source-tile source-tile--auth'> with device name,
  login button (with data-url), and hint text
- HTML-escapes both name and url to prevent XSS
- Export buildAuthTileHTML under '// Federation tiles' comment in module.exports
- Add 5 tests covering: export, article class, device name, login button data-url, HTML escaping

Task: task-2-build-auth-tile-html
2026-03-31 02:37:05 -07:00
Brian Krabach 55c80b4c9b refactor: move intra-function imports to module level in test_frontend_css 2026-03-31 02:31:54 -07:00
Brian Krabach 38a032b2d5 feat: add CSS for source-tile offline and auth-required states
Add Phase 3 source tile component CSS rules to style.css, inserted
before the @media (prefers-reduced-motion) block as specified.

Rules added:
- .source-tile: base flex column layout (centered, 12px gap, 24px padding)
- .source-tile--offline: opacity 0.45, dashed border
- .source-tile--offline .source-tile__name: var(--text-dim) color
- .source-tile--offline .source-tile__badge: var(--err) bg, bold, uppercase
- .source-tile--offline .source-tile__last-seen: 11px font, dim color
- .source-tile--auth: var(--warn) border-color, dashed border
- .source-tile__name: 15px/600 font, var(--text) color
- .source-tile__login-btn: accent bg, 8px 20px padding, opacity transition
- .source-tile__login-btn:hover: opacity 0.85
- .source-tile__login-btn:focus-visible: 2px solid accent outline
- .source-tile__hint: 11px font, var(--text-muted) color

grep -c 'source-tile' style.css returns 11 lines (all required selectors present).

Tests: 15 new tests added to test_frontend_css.py covering all selectors,
properties, positioning before reduced-motion block, and brace balance.
All 150 CSS tests pass.
2026-03-31 02:23:24 -07:00
Brian Krabach 7100fd881e style: apply ruff formatting to test_frontend_css.py (wrap long assertion) 2026-03-31 02:13:10 -07:00
Brian Krabach 918d92386f test: add Phase 2 integration tests — export verification and e2e pipeline (task-19) 2026-03-31 01:56:05 -07:00