186 lines
8.2 KiB
Markdown
186 lines
8.2 KiB
Markdown
# 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.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).
|