Commit Graph
5 Commits
Author SHA1 Message Date
ducoterra f51d0fe9ae phase: 05_autohide_stage_controls
Build and Push Containers / test (push) Successful in 32s
Build and Push Containers / build-and-push-app (push) Successful in 8s
**Phase 05 final verification pass — all green.**

- Verified implementation in tree: staged `.cap`/`.actions` hidden at rest with container+children `pointer-events: none` (`app.css`); `revealStageControls()` with 3 s idle timer, `pointermove`/`pointerdown` + wall-keypress wiring, `unstage()` timer/attribute cleanup (`watch.js`); `tests/e2e/test_stage_autofade.py` (5 tests) present; `test_keyboard_wall.py` rest-state assertion updated per task 01.
- Gates: `uv run ruff check . && uv run ruff format --check .` — clean; `uv run pyright` — 0 errors; `uv run pytest` — **274 passed, 4 skipped** (realdisplay-gated), coverage **95%** (>90% ✓); `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` — **3 passed, 1 skipped** (documented platform skip: no audio source on host).
- Dedicated phase suite `uv run pytest tests/e2e/test_stage_autofade.py` — **5/5 in isolation**.
- Criteria: idle opacity 0 + bottom-edge hit probe ✓; mouse reveal → 1 → idle fade → 0 ✓; keypress reveal + Exit visible/clickable/unstages ✓; grid rules byte-for-byte unchanged (CSS diff touches only the staged block; grid guard tests green) ✓; suite green, coverage >90% ✓; phases 01–04 suites all green (no regression) ✓.
- Documentation: README.md reviewed in full — all commands, env vars (incl. `DEBUGPY_*`, 25 Mbit/s default), `--allow-cidr`, kiosk/share key tables, index-page and auto-hide descriptions match the code; the only stale "12 Mbit/s" default lives in locked PLAN.md §6, README is correct — no change made. AGENTS.md: all facts (gates, test names, starlette 1.10, e2e flags, ~95% coverage) current — verified, no changes needed.
- Notable: no code changes were required this pass — the phase work was already complete and correct; nothing to fix.
- Next pending phase: none — phase 05 is the last in `todo/`.
2026-11-02 19:41:54 -05:00
ducoterra bf027672b5 phase: 04_reachable_share_cta
Build and Push Containers / test (push) Successful in 47s
Build and Push Containers / build-and-push-app (push) Successful in 10s
All verification is complete. The pending working-tree changes are exactly the phase 04 work (CSS, tests, the README narrow-screen line, the AGENTS.md "Two→Three pages" fix) — all accurate.

## Final report

**Verified (final pass — all 4 task files present in `complete/`):**
- Gate 1: `uv run ruff check . && uv run ruff format --check .` → all checks passed, 29 files formatted
- Gate 2: `uv run pyright` → 0 errors
- Gate 3: `uv run pytest` → **269 passed, 4 skipped** (realdisplay-gated), coverage 95%
- Gate 4: `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` → **3 passed, 1 skipped** (host delivers no audio source — documented in module docstring)

**Completion criteria:**
1. `#start` elementFromPoint at the 11 required sizes — ✅ `TestIdleShareButtonReachable` sweeps 12 viewports (superset); 12/12 passed
2. `#stop` clickable while sharing — ✅ `TestLiveStopButtonReachable` (320×568, 390×844, 1280×450) passed, incl. placeholder-hidden + livetag assertions
3. Single-cell stage guard — ✅ `test_the_share_stage_is_one_cell` + `test_the_stage_has_a_floor_and_a_ceiling` passed; CSS has `grid-template: 1fr / 1fr`, `.stage > * { grid-area: 1 / 1 }`, `min-height: 264px`, `max-height: min(70svh, 640px)` + vh fallback, `.placeholder { max-height: 100%; overflow: auto }`, `align-content: safe center`
4. Live video fills stage, rounded corners, `#livetag` top-left — ✅ verified by live-browser probe: video fills stage (decoded), stage radius 14px, livetag top-left, placeholder hidden
5. README accurate, AGENTS.md current, suite 100% green — ✅
6. Phases 01–03 unchanged — ✅ `test_keyboard_wall` + `test_audio_share` + `test_sender_fidelity`: 48 passed

**Documentation:** README.md reviewed in full (page table, quickstart, all 12 config defaults vs `config.py`, CLI flags, secure-context/wayland/audio sections, kiosk key table vs `watch.js`/`share.js`, dev commands) — **consistent, no fixes needed**. AGENTS.md verified statement-by-statement (commands, routes, test names, browser/realdisplay flags) — **no stale statements; no changes needed** (the pending "Two→Three pages" fix is correct).

**Decisions:** criterion 4 had no dedicated test, so verified directly with a throwaway probe reusing the e2e harness (script in /tmp, not committed). No live infrastructure touched. No defects found in prior work.

**Next pending phase:** none — `todo/` is empty of other phases.
2026-11-01 23:26:36 -05:00
ducoterra 1da466a412 phase: 02_sharp_capture
Build and Push Containers / test (push) Successful in 46s
Build and Push Containers / build-and-push-app (push) Successful in 8s
**Phase 02 final verification pass — complete, 100% green.**

**Verified (all 4 tasks in `complete/02_sharp_capture/`):**
- Task 1: `pickSurface()` requests `screen × devicePixelRatio` (capped 3840×2160) instead of window size
- Task 2: `degradationPreference: "maintain-resolution"` + per-leg `maxBitrate`, re-applied after `setLocalDescription` *and* on `connected` (both call sites asserted in `test_app.py`)
- Task 3: default cap raised to 25 Mbit/s — `config.py` constant, `net.js` offline fallback, `.env.example`, README, and the drift-guard test all agree
- Task 4: realdisplay sharpness floor (viewer decoded width ≥ 1152px = 90% of Xvfb 1280px) in `test_sharp_picture_reaches_the_wall`

**Gates (all green):**
- `uv run ruff check . && uv run ruff format --check .` → clean
- `uv run pyright` → 0 errors
- `uv run pytest` → 231 passed, 3 skipped (realdisplay), coverage 95%
- `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` → 3 passed on a real Xvfb + headed Chromium

**Completion criteria:** (1) 90% width floor asserted on viewer's decoded pixels — PASS · (2) `/api/config` `maxBitrateBps: 25000000` default, net.js fallback agrees — PASS · (3) `#stat-resolution` matches viewer-decoded size incl. documented window downscale (1100×620, 1280×720 fake-device tests) — PASS · (4) both suites green, aspect-ratio guard (`1280/800` ± 0.06 + `≠ 1280×720`) still distinguishes real from fake capture — PASS · (5) README Configuration documents `25000000` — PASS; `PLAN.md` §6 "default 12 Mbit/s" left untouched as instructed — **flag for the user to amend**.

**Docs:** README — fixed suite timing `~20s → ~50s` (measured 49s); all other claims verified (CLI flags, dev extra, config table, kiosk key table vs. `watch.js`/`share.js` routers, kiosk commands vs. `docs/kiosk-silverblue.md`). AGENTS.md — same stale timing fixed, nothing else stale (all referenced test names/files exist).

**Notable:** `docs/kiosk-silverblue.md` + its scripts use `mirror.reeselink.com` while README/SECURITY.md/Containerfile use `mirror.reeseapps.com` — a pre-existing cross-file mismatch I could not resolve from the repo (both in same-day commits; live-infra fact). Left untouched; user should confirm the live kiosk URL. No live changes made.

**Next pending phase:** `03_audio_share`.
2026-11-01 21:41:28 -05:00
ducoterra 7988da2b93 feat: decide who may connect at all, by source address
mirror has no accounts, so on a real network the address a connection arrives
from is not a nicety — it is the entire access control. Before this change the
answer was "everyone who can open a TCP connection", which is fine for a laptop
demo and alarming for a box someone leaves running.

`mirror serve --allow-cidr 10.2.0.0/22` (or MIRROR_ALLOW_CIDRS) now restricts
pages, the JSON API and the signaling socket to the networks named, and it is
strict where it needs to be: a malformed CIDR exits 2 with one line instead of
starting a server whose list silently failed to parse, because the two
outcomes of that typo are "nobody can connect" and "everybody can connect".
Empty remains the only permissive state, on purpose.

The rules that matter live in src/mirror/access.py and are the ones that get
either outcome:

  * X-Forwarded-For is believed only from MIRROR_TRUSTED_PROXIES (loopback by
    default), and only its left-most value. An untrusted peer that sends the
    header gets judged on its own address — there is a test for exactly that,
    because it is the shape of a bypass.
  * The socket is closed before it accepts, so a refused address never creates
    a roster entry to clean up behind it.
  * An address that cannot be parsed fails closed, and a v6 address is never
    "inside" a v4 network (comparing the two raises TypeError, which would be
    a 500 on a dual-stack host).
  * /healthz is exempt: the container health probe comes from loopback, which
    a production list otherwise excludes.

The refused client is told why, and which address it was judged as — "403" in
front of a proxy is undiagnosable otherwise. Browsers get a page, fetches get
JSON, and the page is self-contained because a refused address cannot load
/static/app.css any more than it can load a page. net.js keys its own notice on
the /api/config 403 and stops: without that the pill reads "offline,
reconnecting in 32s" forever for a decision that will never change.

The static files drop their only inline script and style attribute (the landing
page's counter is /static/home.js, its fine print is .fineprint) so the
deployment can send `script-src 'self'; style-src 'self'` with no
'unsafe-inline'. That policy was verified through a real nginx edge carrying the
exact header set — capture through SRTP to decoded, painted frames at
1280x720, eight distinct, zero securitypolicyviolation events — which also
caught the edge sending `Cache-Control: no-store, no-store` and needed
proxy_hide_header, since nginx appends rather than replaces.

24 tests added; 176 pass, 2 skipped, pyright and ruff clean.

LLM-Generated: true
2026-11-01 17:47:31 -05:00
ducoterra 7359ee4e07 test: four-rung suite that asserts on decoded pixels, not log lines
130 tests, ~20s, 94% coverage. Four rungs, each proving something the one
below it cannot:

  registry   admission, fan-out, signal authorization and teardown, no I/O
  wire       camelCase round-trip, malformed frames, WebSocket lifecycle
  e2e        real capture -> encode -> SRTP -> decode -> painted frames
  realdisplay the actual X11 capture backend on a private Xvfb display

The e2e rung drives Chromium's fake media device, which produces a genuine
moving 1280x720 monitor stream, so the assertion is that a watcher's <video>
has a size and that its pixels change over time -- a frozen or never-started
peer connection fails the suite, which a "signal arrived" assertion would
not have caught.

Findings encoded as flags and comments, because each cost a debugging cycle:
the headless shell has no media stack at all (use channel="chromium");
mediaDevices is undefined off a secure context (serve over 127.0.0.1); and
--use-fake-ui-for-media-stream breaks real X11 capture by approving the
request without ever selecting a source, so the realdisplay suite selects
the source explicitly instead.

The realdisplay fixture strips WAYLAND_DISPLAY and DBUS_SESSION_BUS_ADDRESS
from the browser environment. Without that, a headed Chromium reaches
xdg-desktop-portal, mirrors the developer's actual desktop and raises a
permission dialog on their real screen -- which is exactly what happened
before this was isolated. Skipped unless MIRROR_REALDISPLAY=1.

Also guards the frontend's structure: scripts must parse as ES modules,
every id a script looks up must exist in its page, ids must not collide
across pages that share one stylesheet, and nothing may load from a CDN.

LLM-Generated: true
2026-11-01 16:54:28 -05:00