# 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://:1471/api/pager/display/screen.ws` — streams binary RGBA framebuffer frames. Requires the daemon `AUTH_` cookie; returns `401 Unauthorized` without it. - Keys: `ws://:1471/api/pager/input/keys.ws` — text frames; same cookie. - Both handshakes verified returning `101 Switching Protocols` with a valid `AUTH_=` 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 `` 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 `` (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, ``, 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.data` → `new 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).