Files
Mark-VIII/docs/superpowers/specs/2026-08-11-virtual-pager-dock-design.md
T
2026-08-11 20:24:24 -07:00

8.2 KiB
Raw Blame History

Virtual Pager Dock — Design Spec

  • Date: 2026-08-11
  • Status: Approved
  • Owner: WiFi Pineapple Pager expansion project
  • Applies to: payload/user/general/pager-webui/www/ (front-end only) and scripts/dev_proxy.py (dev config)

1. Goal

Add a Virtual Pager button in the topbar next to the existing Terminal button in the Pager WebUI (http://172.16.52.1:8080/). Clicking it opens a bottom-docked panel that shows a live, interactive replica of the Pager device: the physical-device graphic with the real-time screen in the middle and clickable directional/A/B buttons — matching the virtual pager view of the Pager's stock UI at http://172.16.52.1:1471/.

Behavior mirrors the existing Terminal dock: open/close toggling, docked panel, connect/disconnect of WebSockets on open/close.

2. Stock UI Reference (verified 2026-08-11)

Source: http://172.16.52.1:1471/ (captured to %TEMP%\opencode\pager-virtual-pager.html, 141,707 bytes).

2.1 Connections

  • Screen: ws://<host>:1471/api/pager/display/screen.ws — streams binary RGBA framebuffer frames. Requires the daemon AUTH_<serverid> cookie; returns 401 Unauthorized without it.
  • Keys: ws://<host>:1471/api/pager/input/keys.ws — text frames; same cookie.
  • Both handshakes verified returning 101 Switching Protocols with a valid AUTH_<serverid>=<token> cookie.
  • Cookies are host-scoped (not port-scoped), so a browser that has logged into 172.16.52.1:8080 already sends the cookie to 172.16.52.1:1471. This is the same mechanism the existing Terminal WS uses (direct :1471 connection, no backend relay), and requires no backend changes.

2.2 Screen framebuffer

  • Resolution 480 × 222, stride 480 * 4 = 1920, expected 1920 * 222 = 426,240 bytes per frame (4 bytes/pixel RGBA).
  • Client renders each frame: copy RGBA into a hidden 480×222 canvas (createImageData/putImageData), canvas.toDataURL('image/png') into the <img id="pager"> screen element.

2.3 Buttons / keys

Button image click sends these strings over the keys WS:

Control Sent key
LEFT ArrowLeft
UP ArrowUp
RIGHT ArrowRight
DOWN ArrowDown
A Enter
B Escape

Physical keyboard: when the pager screen is focused (or body, with no other control focused) the stock UI sends e.key on keydown (no repeats). Buttons show a brief .pressed feedback (brightness filter).

2.4 Device graphic

<table id="pager_ui"> (745 × 531) built from 24 image slices + spacer.gif, button images LEFT.png, UP.png, RIGHT.png, DOWN.png, A_Button.png, B_Button.png, and a 480×222 screen surface with hidden canvas and a #pager_error overlay. Images are static and fetchable without auth from http://172.16.52.1:1471/images/. Identical files already exist in wifipineapplepager/payloads/library/user/remote_access/nautilus/www/images/ (sizes match byte-for-byte); they will be copied from there and hash-verified against the stock UI.

Scaling: the stock UI scales the graphic with CSS zoom (scale = min(1, availableWidth / 745)), transform-origin: top center.

3. Design

3.1 index.html

  • Add #pager-btn ("Virtual Pager", btn ghost) next to #terminal-btn.
  • Add #pager-panel dock after #terminal-panel, mirroring its structure:
    • #pager-bar — title "Virtual Pager" + #pager-close button.
    • #pager content area containing the pager graphic table with the live screen, hidden canvas, and error/retry overlay.
  • Load js/pager.js after js/terminal.js.

3.2 app.css

  • #pager-btn: same style as #terminal-btn; .active state for both.
  • #pager-panel: fixed bottom dock like #terminal-panel but height: 540px; reuses the term-up slide-in animation and a shared .dock utility where practical. Content centered; overflow hidden.
  • Pager graphic scale-to-fit: a wrapper whose zoom/transform scales the 745×531 graphic to the available panel width, never upscaling (scale = min(1, availableWidth / 745)), per §2.4.
  • .pager-btn cursor/user-select/pressed feedback (stock rules).
  • #pager_error overlay styles (centered, dark overlay, Reconnect button).

3.3 js/pager.js (new, mirrors js/terminal.js)

Module Pager with:

  • SCREEN_WIDTH = 480, SCREEN_HEIGHT = 222, FB_STRIDE = 1920.
  • ensure() — lazily grab #pager-panel, canvas, <img id="pager">, build the graphic table (static innerHTML from bundled images), wire button clicks.
  • toggle() — show/hide #pager-panel, toggle #pager-btn.active, connect/disconnect.
  • Screen WS: binary event.datanew Uint8Array(buffer)renderRGBAFrame(bytes) (reject frames < FB_STRIDE * SCREEN_HEIGHT). On error/close: hide screen, show error overlay with Reconnect.
  • Keys WS: sendKey(name) helper; button clicks send the §2.3 strings.
  • Keyboard: a keydown listener active only while the dock is open; skips when focus is in an INPUT/TEXTAREA/SELECT or the terminal; captures ArrowLeft/ArrowUp/ArrowRight/ArrowDown/Enter/Escape (preventDefault on the arrows to stop page scroll) and sends them via keys WS. Letter shortcuts (D/C/P/R/L/M) and backtick (terminal toggle) keep working — backtick closes the pager dock via the replace-on-open behavior.
  • disconnect() closes both WS.
  • window.addEventListener('resize') re-applies scale-to-fit while open.

3.4 app.js

  • Wire #pager-btn and #pager-close to Pager.toggle() (guarded with typeof Pager === 'undefined' like the terminal).
  • Replace-on-open: a small helper showDock(name) that, when opening pager, calls Term.close() if Term is defined and its panel is open, and vice versa. Refactor the terminal wiring to go through showDock('terminal') so both directions use the same rule. (Term gains a close()/isOpen() helper in terminal.js.)
  • Keyboard shortcut g (or none) is not required; the button is the entry point. Backtick toggles the terminal and closes the pager dock (per replace-on-open).

3.5 config.js / dev_proxy.py

  • config.js: add pagerScreenWs: '', pagerKeysWs: ''.
  • app.js: read cfg.pagerScreenWs || ('ws://' + location.hostname + ':1471/api/pager/display/screen.ws') and the keys equivalent; expose as App.pagerScreenWs / App.pagerKeysWs.
  • dev_proxy.py _serve_dev_config: emit both fields pointing at the pager host (same shape as terminalWs).

3.6 Assets

  • Copy from the nautilus payload into www/assets/pager/: virtual_pager_01..24.png, LEFT.png, UP.png, RIGHT.png, DOWN.png, A_Button.png, B_Button.png, spacer.gif.
  • Hash-verify each against the stock UI (http://172.16.52.1:1471/images/…) during implementation; if any differ, use the stock bytes instead.

3.7 Error handling

  • Screen WS error/close: show #pager_error ("Lost connection to virtual pager") with a Reconnect button that calls connect().
  • Keys WS is best-effort: if not OPEN, button presses are ignored (stock behavior).
  • If WS construction throws (unreachable daemon), write a message into the error overlay rather than throwing in app.js.

4. Testing

  • Unit (Python): none required — no backend changes.
  • Static review: pager.js frame renderer bounds check; button→key mapping table; keyboard guard conditions; replace-on-open logic.
  • On-device smoke (per §8 of the main webui spec):
    1. Log in at http://172.16.52.1:8080/; Virtual Pager button present next to Terminal.
    2. Open Virtual Pager: dock slides up, screen streams live, graphic scales to fit width.
    3. Click LEFT/UP/RIGHT/DOWN/A/B — screen responds; physical arrow/Enter/Escape also drive it; typing in an input/terminal does not.
    4. Open Terminal while Pager open → Pager dock closes (replace-on-open) and vice versa.
    5. Kill/reconnect network to the daemon → error overlay + Reconnect recovers.
    6. Backtick still toggles the terminal and closes the Pager dock.

5. Out of scope

  • The stock page's embedded shell terminal and terminal-shortcut grid (the WebUI already has a Terminal dock).
  • The Pager Skinner / virtual-pager theme features.
  • Any backend relay for the pager WS (direct :1471 connection, same as the Terminal).