8.2 KiB
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) andscripts/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 daemonAUTH_<serverid>cookie; returns401 Unauthorizedwithout it. - Keys:
ws://<host>:1471/api/pager/input/keys.ws— text frames; same cookie. - Both handshakes verified returning
101 Switching Protocolswith a validAUTH_<serverid>=<token>cookie. - Cookies are host-scoped (not port-scoped), so a browser that has logged into
172.16.52.1:8080already sends the cookie to172.16.52.1:1471. This is the same mechanism the existing Terminal WS uses (direct:1471connection, no backend relay), and requires no backend changes.
2.2 Screen framebuffer
- Resolution
480 × 222, stride480 * 4 = 1920, expected1920 * 222 = 426,240bytes 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-paneldock after#terminal-panel, mirroring its structure:#pager-bar— title "Virtual Pager" +#pager-closebutton.#pagercontent area containing the pager graphic table with the live screen, hidden canvas, and error/retry overlay.
- Load
js/pager.jsafterjs/terminal.js.
3.2 app.css
#pager-btn: same style as#terminal-btn;.activestate for both.#pager-panel: fixed bottom dock like#terminal-panelbutheight: 540px; reuses theterm-upslide-in animation and a shared.dockutility 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-btncursor/user-select/pressed feedback (stock rules).#pager_erroroverlay 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.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
keydownlistener 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-btnand#pager-closetoPager.toggle()(guarded withtypeof Pager === 'undefined'like the terminal). - Replace-on-open: a small helper
showDock(name)that, when openingpager, callsTerm.close()ifTermis defined and its panel is open, and vice versa. Refactor the terminal wiring to go throughshowDock('terminal')so both directions use the same rule. (Termgains aclose()/isOpen()helper interminal.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: addpagerScreenWs: '',pagerKeysWs: ''.app.js: readcfg.pagerScreenWs || ('ws://' + location.hostname + ':1471/api/pager/display/screen.ws')and the keys equivalent; expose asApp.pagerScreenWs/App.pagerKeysWs.dev_proxy.py_serve_dev_config: emit both fields pointing at the pager host (same shape asterminalWs).
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 callsconnect(). - 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.jsframe renderer bounds check; button→key mapping table; keyboard guard conditions; replace-on-open logic. - On-device smoke (per §8 of the main webui spec):
- Log in at
http://172.16.52.1:8080/; Virtual Pager button present next to Terminal. - Open Virtual Pager: dock slides up, screen streams live, graphic scales to fit width.
- Click LEFT/UP/RIGHT/DOWN/A/B — screen responds; physical arrow/Enter/Escape also drive it; typing in an input/terminal does not.
- Open Terminal while Pager open → Pager dock closes (replace-on-open) and vice versa.
- Kill/reconnect network to the daemon → error overlay + Reconnect recovers.
- Backtick still toggles the terminal and closes the Pager dock.
- Log in at
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
:1471connection, same as the Terminal).