Added 18px to font size select. Dialog click handler checks
e.target === settingsDialog (click landed on ::backdrop area,
not dialog content) and closes settings.
Implement new public endpoint that returns device name and version without authentication required. The auth exemption was already in place from task 4.
- 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>
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