Commit Graph

146 Commits

Author SHA1 Message Date
Brian Krabach 60300f64ea fix(auth): enforce mode 0700 on config dir mkdir to match docstring 2026-03-28 20:54:57 -07:00
Brian Krabach c7050a2b02 refactor(auth): eliminate dead _config_dir() and trim docstring
- Wire _config_dir() into generate_and_save_password() so the helper is no longer dead code; the 0700 directory mode is now enforced correctly
- Trim module docstring to reflect what the file actually contains (password file management only)
2026-03-28 20:52:59 -07:00
Brian Krabach 3b248e2a63 feat(auth): password file management
Add muxplex/auth.py with password file management functions:
- get_password_path(): returns ~/.config/muxplex/password
- load_password(): reads password file, returns None if not found
- generate_and_save_password(): generates random password, writes with 0600 permissions

Add muxplex/tests/test_auth.py with 5 tests covering all functions.

Co-authored-by: Amplifier <amplifier@invalid>
2026-03-28 20:49:04 -07:00
Brian Krabach 4cd013fcc5 chore: add python-pam and itsdangerous dependencies 2026-03-28 20:42:33 -07:00
Brian Krabach 0dbf8ca2cb docs: add authentication design document
Covers PAM + password auth modes, localhost bypass, session cookies,
service installation (systemd/launchd), and testing strategy.
2026-03-28 19:26:33 -07:00
Brian Krabach c6a5507daf revert: remove JS touch scroll handlers — back to mouse-only baseline
Removing terminal WheelEvent dispatch and sidebar scrollTop handlers.
CSS fixes retained (touch-action:pan-y, overscroll-behavior:contain,
flex-shrink:0). Testing mouse-only baseline across all devices before
deciding on the right mobile scroll approach.
2026-03-28 16:38:54 -07:00
Brian Krabach b2b9fd1746 fix: smooth Android terminal scroll via delta accumulation
Old: variable deltaY*3 per touchmove — Android batches events causing
burst-scroll then pause (jumpy). New: accumulate pixels across events,
fire fixed deltaY=120 per SCROLL_PX pixels. Each scroll step is
identical regardless of when Android delivers touchmove events.

SCROLL_PX=20 → ~40px swipe = 2 scroll clicks. Accumulator resets on
touchstart and touchend. macOS/iPad/mouse-wheel path unchanged.
2026-03-28 16:19:30 -07:00
Brian Krabach c1701bf26b fix: sidebar-item flex-shrink:0 — cards were compressing to fit, eliminating scroll
Without flex-shrink:0, flex compresses all sidebar cards proportionally
when the container is shorter than their total height. Cards visually
fit the screen with no overflow — scrollTop stays 0, touch handler
has nothing to scroll. One property restores the intended overflow.
2026-03-28 16:04:02 -07:00
Brian Krabach 383b28c85e fix: sidebar touch scroll via scrollTop — bypasses broken CSS path
position:fixed + overflow:hidden on .session-sidebar in mobile overlay
mode prevents Android Chrome from routing native touch-scroll to the
child .sidebar-list (overflow-y:auto). Direct scrollTop manipulation
always works. Mirrors the terminal WheelEvent approach: take JS control
rather than relying on native CSS touch-scroll routing.
2026-03-28 15:52:37 -07:00
Brian Krabach 195912edf9 fix: touch scroll dispatches WheelEvent instead of scrollLines
scrollLines() only moves xterm.js's local scrollback buffer (outside).
WheelEvent dispatch goes through xterm.js's mouse reporting path —
if tmux mouse mode is on, escape sequences reach the PTY so tmux and
applications (vim, less, etc.) handle the scroll (inside).

Same behavior as mouse wheel on desktop.
2026-03-28 15:01:01 -07:00
Brian Krabach 3d91bd5c68 feat: touch scroll for terminal and sidebar on mobile
Terminal: add touchstart/touchmove/touchend IIFE on #terminal-container.
Vertical swipe delta translated to _term.scrollLines(n). touchmove is
non-passive (required for e.preventDefault to block page scroll while
swiping inside terminal). mouse wheel scroll unaffected (separate wheel
event path in xterm.js).

Sidebar: add touch-action:pan-y + overscroll-behavior:contain to
.sidebar-list. Tells browser explicitly that vertical panning is allowed
and prevents scroll chaining when reaching list ends.
2026-03-28 14:37:57 -07:00
Brian Krabach 4ffdc1bd99 fix: three mobile/WSL bugs reported by tester
- Mobile idle tiles: display:none → height:36px so session content is
  visible (all sessions are 'idle' with zero bells — was blank on iPhone)
- .tile-pre dead CSS: merge font/color rules into .tile-body pre (the
  actual HTML selector) so snapshot text renders with monospace + muted
  color; update mobile active-tier selector to match
- systemd PATH: strip /mnt/ entries from PATH before writing service
  unit file — WSL paths with spaces caused systemd to reject the line
2026-03-28 09:03:33 -07:00
Brian Krabach 38c78fb575 fix: WebSocket proxy drops text frames — receive_bytes() silently fails on auth token
terminal.js sends the ttyd auth token as a TEXT WebSocket frame. The proxy's
client_to_ttyd() called receive_bytes() which fails silently on text frames,
swallows the exception, and exits — auth never reached ttyd, ttyd never
started streaming, resulting in permanent black screen and reconnect loop.

Fix: use receive() and dispatch on message type, handling both bytes and text.
2026-03-28 08:50:43 -07:00
Brian Krabach 6cf54efcb5 docs: use uv pip install in development setup instructions 2026-03-28 06:52:39 -07:00
Brian Krabach 82ec323aaa fix: address pre-merge review findings — stale identity strings, pyright error, fragile test path, formatting, README accuracy
Required fixes:
- main.py: update module docstring, startup comment, and FastAPI title from
  'tmux-web coordinator' / 'coordinator service' → 'muxplex' (4 locations)
- test_api.py: replace hasattr/BaseRoute pattern with isinstance(r, (APIRoute,
  APIWebSocketRoute)) to satisfy pyright (union-attr error eliminated)

Recommendations applied:
- test_api.py + test_cli.py: auto-formatted with ruff (blank-line normalisation)
- test_cli.py: replace exec(Path('muxplex/__main__.py').read_text()) with
  importlib.util.find_spec() to obtain the absolute path — no longer assumes
  pytest is invoked from a specific working directory
- README.md: fix architecture table — WebSocket proxy moved from ttyd.py
  description to main.py; ttyd.py now correctly described as lifecycle only

All 178 tests pass; python_check clean (0 errors, 0 warnings).
2026-03-28 02:47:18 -07:00
Brian Krabach 110a503df0 docs: fix JS test command and add terminal.js to architecture tree 2026-03-28 02:38:28 -07:00
Brian Krabach 18365e246c docs: rewrite README with uvx/uv install instructions and full usage guide 2026-03-28 02:34:11 -07:00
Brian Krabach 23d1d9c113 chore: retire requirements.txt in favor of pyproject.toml
Addresses code quality suggestion from review: requirements.txt was a
redundant secondary source of truth. Dependencies are now exclusively
managed in pyproject.toml with proper runtime/dev separation.

- requirements.txt: replaced with a comment redirecting to pip install -e '[dev]'
- README.md: updated install command from pip install -r requirements.txt
  to pip install -e '[dev]'
2026-03-28 02:31:15 -07:00
Brian Krabach 10906ec4b0 chore: configure pyproject.toml for distribution (entry point, deps, hatchling build) 2026-03-28 02:26:07 -07:00
Brian Krabach c2087083c2 feat: add CLI entry point (muxplex serve, muxplex install-service) and __main__.py 2026-03-28 02:18:58 -07:00
Brian Krabach a70e8f6f52 feat: add WebSocket proxy route to replace Caddy, update bell hook port to configurable SERVER_PORT 2026-03-28 02:11:24 -07:00
Brian Krabach 74b63033d7 refactor: rename coordinator → muxplex package, move frontend inside as package data 2026-03-28 02:02:50 -07:00
Brian Krabach be26d40a31 docs: add distribution packaging implementation plan 2026-03-28 01:57:30 -07:00
Brian Krabach cb38f11588 refactor: replace var with const and fix misleading closure comment in terminal.js 2026-03-28 01:32:45 -07:00
Brian Krabach 26e2baa89c fix: close stale WebSocket on session switch to prevent double output and crash loop
Before: openTerminal() never closed the previous WebSocket. After A→B→A,
two WS connections to session A both wrote to _term → double keystroke echoes.
Rapid switches triggered a CONNECTING-state send error and reconnect loop.

After: openTerminal() nulls _currentSession, clears reconnect timer, and
closes _ws before opening a new connection. connect() event handlers now
capture the WS instance in a local const and check 'ws !== _ws' on entry
so stale connections silently ignore all events.

Tests: three new regression tests verify:
- openTerminal() calls close() on the previous WS (Bug 1)
- stale open handler does not send on new WS after switch (Bug 2)
- stale close handler does not schedule reconnect after switch (Bug 2)
2026-03-28 01:25:29 -07:00
Brian Krabach e2d4282b5d fix: sidebar active state uses inset box-shadow (no layout shift) and font-mono token 2026-03-28 01:19:25 -07:00
Brian Krabach f1c28a4d06 style: sidebar cards use outlined tile aesthetic with gap, rounded corners, and fade gradient 2026-03-28 01:12:27 -07:00
Brian Krabach 41ff94afb1 docs: add session sidebar implementation plan 2026-03-27 18:06:44 -07:00
Brian Krabach f41061f85f fix: correct sidebar element IDs, add missing CSS classes, and align test mocks
Three-part consistency defect identified in code review:

1. HTML: Four sidebar elements missing CSS class attributes so all CSS
   rules targeting .session-sidebar, .sidebar-toggle-btn,
   .sidebar-collapse-btn, and .sidebar-list applied to zero elements.
   Added class= attributes to all four.

2. JS: initSidebar(), toggleSidebar(), and bindSidebarClickAway() called
   $('sidebar') and $('collapse-btn') — neither ID exists in the DOM.
   Corrected to $('session-sidebar') and $('sidebar-collapse-btn').

3. Tests: initSidebar/toggleSidebar mocks matched the wrong IDs, masking
   the defect. Updated all six mock conditions to match corrected IDs.

4. Coverage gap: test_html_element_classes did not verify new sidebar
   elements carry CSS classes. Added four entries for session-sidebar,
   sidebar-toggle-btn, sidebar-collapse-btn, and sidebar-list — these
   entries would have caught Defect 1 on Task 1's test run.

All 136 JS + 167 Python tests pass. Zero regressions.
2026-03-27 18:02:29 -07:00
Brian Krabach b62bd3e361 feat: add overlay click-away handler for sidebar on narrow screens 2026-03-27 17:49:07 -07:00
Brian Krabach cf6f7daf45 feat: wire renderSidebar into poll loop for live sidebar updates
Added renderSidebar(sessions, _viewingSession) call in pollSessions function after renderGrid(sessions) and before handleBellTransitions(prev, sessions), so sidebar updates on every poll tick alongside dashboard grid. Added test verifying sidebar-list innerHTML is set during pollSessions in fullscreen mode.

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-03-27 17:44:19 -07:00
Brian Krabach 93dc92484b feat: pre-render sidebar on openSession before first poll tick 2026-03-27 17:38:36 -07:00
Brian Krabach 9f60ead3d2 feat: wire sidebar toggle button click handlers 2026-03-27 17:23:53 -07:00
Brian Krabach 0c39a79b8a feat: add initSidebar and toggleSidebar with localStorage persistence 2026-03-27 17:18:19 -07:00
Brian Krabach bba862fba3 feat: add renderSidebar — populates sidebar list from session data 2026-03-27 17:11:18 -07:00
Brian Krabach 730ed68a30 feat: add buildSidebarHTML — session card HTML builder for sidebar 2026-03-27 17:05:40 -07:00
Brian Krabach 67da2e9aa2 feat: add responsive sidebar overlay at <960px and reduced-motion support 2026-03-27 17:00:07 -07:00
Brian Krabach 4e7e461077 feat: add sidebar session card styles 2026-03-27 16:53:51 -07:00
Brian Krabach 599b9a8997 feat: add sidebar container styles and collapse animation 2026-03-27 16:48:09 -07:00
Brian Krabach 0f95f5aab0 feat: add view-body flex layout and --bg-surface variable 2026-03-27 16:44:20 -07:00
Brian Krabach 1023c14480 fix: type-safe Tag guard in test and aria-label on sidebar-collapse-btn 2026-03-27 16:40:51 -07:00
Brian Krabach a3f1e2efd8 feat: add sidebar markup, toggle button, and view-body wrapper to expanded view
- Add #sidebar-toggle-btn to expanded-header (after #back-btn, before #expanded-session-name)
  with aria-label='Toggle session list' and hamburger icon &#9776;
- Wrap #terminal-container in div.view-body
- Add #session-sidebar before #terminal-container inside .view-body, containing:
  - .sidebar-header with .sidebar-title 'Sessions' and #sidebar-collapse-btn (chevron &#8249;)
  - #sidebar-list div
- Keep #reconnect-overlay outside .view-body as sibling inside #view-expanded

Tests added:
- test_html_sidebar_toggle_button: verifies placement, aria-label, and order in header
- test_html_view_body_wrapper: verifies .view-body contains #session-sidebar and #terminal-container
- test_html_reconnect_overlay_outside_view_body: verifies #reconnect-overlay is outside .view-body
- test_html_session_sidebar_structure: verifies full sidebar DOM structure

Co-authored-by: Amplifier <amplifier@amplified.dev>
2026-03-27 16:35:43 -07:00
Brian Krabach 95c1ec1ad4 docs: add session sidebar design document 2026-03-27 16:05:57 -07:00
Brian Krabach 604525e64c fix: bell badge sizing, text contrast, and empty timestamp display
- .tile-bell: inline-flex badge (min-width:16px height:16px) so it's a
  circle for single digits, pill for '9+'; was display:inline so
  width/height were ignored → oval shape
- .tile-bell color: #0D1117 (dark on amber) — was inheriting muted gray
  from .tile-meta, making count illegible
- formatTimestamp(null): return '' instead of em-dash '—' which rendered
  as a confusing glyph in the tile meta area
2026-03-27 15:32:39 -07:00
Brian Krabach 50007a6d2a fix: anchor tile preview to bottom so newest terminal content is visible
Previously the <pre> snapshot floated to the top of .tile-body — when
content overflowed, the oldest lines showed and newest were clipped.
Now .tile-body pre is position:absolute;bottom:0 so overflow clips
from the top and the lower-left corner (newest content) is always shown.
2026-03-27 15:17:07 -07:00
Brian Krabach 8234e2ec05 refactor: restructure project into muxplex/ subdir with brand integration
- Move coordinator/, frontend/, Caddyfile, pyproject.toml, requirements.txt,
  docs/ into muxplex/ subdir in prep for packaging/sharing
- Add brand assets to frontend/: favicon.ico, pwa-192/512.png,
  apple-touch-icon.png, wordmark-on-dark.svg
- Update app: title → muxplex, header → wordmark SVG, brand color tokens
  in style.css, manifest.json updated with muxplex name and brand icons
- Add design system: assets/branding/tokens.css (101 CSS custom properties),
  tokens.json (127 tokens), DESIGN-SYSTEM.md (856-line spec)
- Add assets/branding/: SVG sources, rendered PNGs (icons, favicons, PWA, OG)
- Add scripts/render-brand-assets.py for reproducible PNG generation
- Add muxplex/README.md
2026-03-27 15:06:00 -07:00