From 4ffdc1bd9961f66d49cd3f42c669f89bda78ebd3 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sat, 28 Mar 2026 09:03:33 -0700 Subject: [PATCH] fix: three mobile/WSL bugs reported by tester MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- muxplex/cli.py | 6 +++- muxplex/frontend/style.css | 13 ++++++-- muxplex/tests/test_cli.py | 37 +++++++++++++++++++++ muxplex/tests/test_frontend_css.py | 53 ++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 4 deletions(-) diff --git a/muxplex/cli.py b/muxplex/cli.py index 66e8d1b..1134b09 100644 --- a/muxplex/cli.py +++ b/muxplex/cli.py @@ -25,6 +25,10 @@ def install_service(*, system: bool = False) -> None: """Install muxplex as a systemd service.""" executable = sys.executable + _raw_path = os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin") + _safe_path = ":".join(p for p in _raw_path.split(":") if not p.startswith("/mnt/")) + _safe_path = _safe_path or "/usr/local/bin:/usr/bin:/bin" + unit = f"""\ [Unit] Description=muxplex — web-based tmux session dashboard @@ -35,7 +39,7 @@ Type=simple ExecStart={executable} -m muxplex Restart=on-failure RestartSec=5s -Environment=PATH={os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin")} +Environment=PATH={_safe_path} [Install] WantedBy={"multi-user.target" if system else "default.target"} diff --git a/muxplex/frontend/style.css b/muxplex/frontend/style.css index 1134f2f..d6e4a2a 100644 --- a/muxplex/frontend/style.css +++ b/muxplex/frontend/style.css @@ -228,6 +228,13 @@ body { bottom: 0; left: 0; right: 0; + padding: 6px 8px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-dim); + white-space: pre; + overflow: hidden; + margin: 0; } .tile-pre { @@ -678,15 +685,15 @@ body { .session-tile--tier-active { min-height: 60px; } - .session-tile--tier-active .tile-pre { + .session-tile--tier-active .tile-body pre { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - /* Tier 3 — idle: name-only (~44px total) */ + /* Tier 3 — idle: compact preview (36px body) */ .session-tile--tier-idle .tile-body { - display: none; + height: 36px; } .session-tile--tier-idle { min-height: 44px; diff --git a/muxplex/tests/test_cli.py b/muxplex/tests/test_cli.py index 5e5be38..18a0667 100644 --- a/muxplex/tests/test_cli.py +++ b/muxplex/tests/test_cli.py @@ -84,6 +84,43 @@ def test_install_service_system_mode_target(tmp_path, monkeypatch): assert "multi-user.target" in content +def test_install_service_strips_wsl_mnt_paths_from_environment(tmp_path, monkeypatch): + """Fix 3: install_service() must strip /mnt/ paths from Environment=PATH. + + WSL mounts Windows at /mnt/c/, /mnt/d/ etc. Paths like + '/mnt/c/Program Files/dotnet/' contain spaces, causing systemd to + truncate and reject the Environment= line. + """ + from muxplex.cli import install_service + + fake_home = tmp_path / "home" + fake_home.mkdir() + monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home)) + + wsl_path = ( + "/usr/local/bin:/usr/bin:/bin:/mnt/c/Program Files/dotnet:/mnt/d/tools/bin" + ) + monkeypatch.setenv("PATH", wsl_path) + + install_service(system=False) + + unit_path = fake_home / ".config" / "systemd" / "user" / "muxplex.service" + content = unit_path.read_text() + + # Find the Environment=PATH line + env_line = next( + (line for line in content.splitlines() if line.startswith("Environment=PATH=")), + None, + ) + assert env_line is not None, "Environment=PATH line must be present" + assert "/mnt/" not in env_line, ( + f"WSL /mnt/ paths must be stripped from Environment=PATH; got: {env_line!r}" + ) + # Safe paths must still be present + assert "/usr/local/bin" in env_line + assert "/usr/bin" in env_line + + def test_dunder_main_calls_main(): """python -m muxplex must call cli.main().""" import importlib.util diff --git a/muxplex/tests/test_frontend_css.py b/muxplex/tests/test_frontend_css.py index 74702a7..3eb695e 100644 --- a/muxplex/tests/test_frontend_css.py +++ b/muxplex/tests/test_frontend_css.py @@ -534,3 +534,56 @@ def test_css_reduced_motion_sidebar_after_toast(): assert toast_idx < sidebar_idx, ( ".session-sidebar must come after .toast in reduced-motion block" ) + + +# ── Bug-fix regression tests ───────────────────────────────────────────────── + + +def test_idle_tile_body_not_display_none(): + """Fix 1: .session-tile--tier-idle .tile-body must NOT use display:none. + + All sessions are 'idle' when zero bell notifications — display:none caused + a blank screen on mobile (iPhone). + """ + css = read_css() + # Locate the idle rule block + marker = ".session-tile--tier-idle .tile-body" + assert marker in css, "idle .tile-body rule must exist" + idx = css.index(marker) + block_start = css.index("{", idx) + block_end = css.index("}", block_start) + block = css[block_start:block_end] + assert "display: none" not in block, ( + ".session-tile--tier-idle .tile-body must not use display:none " + "(hides all tile content on mobile)" + ) + + +def test_tile_body_pre_has_typography(): + """Fix 2a: .tile-body pre must carry font-family and color declarations. + + Previously those rules were on dead .tile-pre class which was never in HTML. + """ + css = read_css() + marker = ".tile-body pre" + assert marker in css, ".tile-body pre rule must exist" + idx = css.index(marker) + block_start = css.index("{", idx) + block_end = css.index("}", block_start) + block = css[block_start:block_end] + assert "font-family" in block, ".tile-body pre must declare font-family" + assert "color" in block, ".tile-body pre must declare color" + + +def test_mobile_active_tier_targets_tile_body_pre_not_tile_pre(): + """Fix 2b: mobile active-tier selector must be .tile-body pre, not .tile-pre. + + .tile-pre is never applied in HTML; the real element is
 inside .tile-body.
+    """
+    css = read_css()
+    assert ".session-tile--tier-active .tile-body pre" in css, (
+        "mobile active-tier must target .tile-body pre"
+    )
+    assert ".session-tile--tier-active .tile-pre" not in css, (
+        ".tile-pre is a dead class — selector must be removed"
+    )