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.
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.
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.
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.
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'.
- 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
<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()).
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.
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'.
- 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
- 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
- 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
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>
- 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
- 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>
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).
- 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>
- 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
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
- Replace DEFAULT_SETTINGS.copy() with copy.deepcopy(DEFAULT_SETTINGS) in
load_settings() and save_settings() to prevent mutation of module-level
mutable defaults (hidden_sessions list was shared via shallow copy)
- Replace except (json.JSONDecodeError, Exception) with
except (FileNotFoundError, json.JSONDecodeError) to only suppress expected
errors and let unexpected exceptions (PermissionError etc.) propagate
- Add two regression tests:
test_load_does_not_mutate_default_settings: verifies deepcopy isolation
test_load_propagates_non_json_errors: verifies narrow exception handling