4 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 02a816c8dc stop stranding modifiers — every left click was a right click
Reported as "no way to send a left click". `xinput --query-state` on the
display showed `Shift_L` down on the XTEST device with nothing holding it, so
every click reached the game as shift-click, which Minesweeper reads as a right
click. The board flagged instead of opening and nothing about the panel looked
wrong.

Mine, from the keyboard work two commits ago, and two separate mistakes.

── modifiers should never have been sent ──

Press shift, release it while the canvas no longer has focus, and the keyup
never arrives — the key is then down on the server forever. They are also
POINTLESS: `ev.key` is the character the layout already produced, so a shifted
key arrives as `A` and x11vnc presses shift itself to type it. Sending our own
on top was redundant on the good path and load-bearing on the bad one.

`keysymFor` now returns null for them, which also hands them back to the
browser — shift-tab should still move focus out of a game of Minesweeper.

── and nothing released what was held ──

Any key can strand the same way, so the panel now tracks what it has pressed
and releases it on blur and on unmount. Cheap, and the failure it prevents is
invisible from the panel: the server has no way to know a key was released
somewhere it could not see.

The live display is unstuck too — `xdotool keyup` on the modifiers, since the
fix stops it happening again but does not undo the one already stranded.

The general lesson, which is the same one the difficulty command taught: input
synthesised into another program's session is state you now own. Press without
a guaranteed release is a leak, and it does not look like one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
2026-08-24 17:04:05 +00:00
pastilhasandClaude Opus 5 ef74f0c2dd keyboard, and back off the size a quarter step
── keys ──

`rfb.ts` already had a `key()` that nothing called. It now has `keysymFor`,
mapping `KeyboardEvent.key` to an X11 keysym — `key` rather than `code`,
because it is already the character the user's own layout produced and `code`
would need that layout re-implemented here to get back to a symbol. Latin-1
passes straight through, above it uses X's 0x01000000 Unicode offset, and the
named keys come from keysymdef.h.

The canvas is focusable and takes focus on mousedown. Scoping keys to focus is
what stops the panel eating keys meant for the rest of the app — this is one
panel on a dashboard, so a window listener would be a global keyboard trap.
Modified keys and unmapped keys are left alone: cmd-W stays the browser's and
Tab keeps moving focus. preventDefault only for keys actually sent.

Keys reach wine without a window manager — verified by pressing F2 on the
display and watching the board reset. So the README's guess that wine needed a
WM to hand it focus was wrong; Auto Options ignores Escape because that VB6
form has no Cancel button, which is a different thing. Corrected there.

20 keysym cases checked against keysymdef.h, including the off-by-one I put in
my own test first: XK_F2 is 0xffbf, not 0xffc0.

── size ──

Half steps overshot: 2x to 2.5x is too small to too large with nothing in
between. Quarter steps from 2x up, which keeps cells whole for the same reason
halves did — a 16px grid at 2.25x is a 36px cell — and lets the picture track
the panel instead of lurching. Plus a 24px device-pixel margin, because
filling the panel edge to edge is most of what "too large" looks like.

Also: the scale was ignoring the panel's own CSS `zoom`. `devicePixelRatio`
knows nothing about it, so at 75% every cell boundary went back onto fractional
device pixels — the exact defect the snapping exists to prevent, reachable from
a button in the panel header. Effective ratio is now dpr x zoom, read as
getBoundingClientRect().width / offsetWidth.

In a 1300x900 device-pixel panel: 2.5x becomes 2.25x, 1260 to 1134 px wide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
2026-08-23 22:32:10 +00:00
pastilhasandClaude Opus 5 76b6a9c567 correct the pixel shifts — the stream was cyan, not grey
SetPixelFormat asked for 32bpp big-endian with shifts 16/8/0, which is the
pixel 0x00RRGGBB and puts X,R,G,B on the wire. The Raw path copies four bytes
straight into ImageData and forces alpha opaque, so every channel landed one
place along and blue was dropped: window grey rendered cyan, the red LED
digits rendered green, blue "1" cells and the title bar rendered black.

Shifts are now 24/16/8, making the pixel 0xRRGGBB00 and the wire R,G,B,X —
which is what the file already claimed, in two comments, since it was written.

Verified against the live x11vnc socket rather than by reading: probing the
same framebuffer under both formats returns `00 c0 c0 c0` for window grey with
the old shifts and `c0 c0 c0 00` with the new ones, and `00 ff 00 00` vs
`ff 00 00 00` for the digits. x11vnc honours a shift of 24 without complaint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
2026-08-23 21:03:44 +00:00
pastilhasandClaude Opus 5 f3cc1242c4 minesweeper: extracted from the platform
Minesweeper X under wine on a headless display, as a plugin. It was
/api/minesweeper, a Dashboard screen, an officerdev panel, a permission
registry entry and a hand-written websocket provider in server.tsx.

The socket is why it had to live in the platform: Bun's route table is fixed
at serve(), so nothing installed later could add one. That ceiling is gone,
and this is the first plugin through the per-plugin relay.

Three things changed rather than moved:

The sidecar owns the session. The platform used to spawn bin/session.sh
detached, so the game outlived every restart, nothing supervised it, and a
crashed run left wine processes holding the prefix — the next start hung
rather than failed. PM2 supervises a sidecar, and stop() uses 'wineserver -k'
scoped by WINEPREFIX instead of 'pkill -f wineserver', which would have killed
every other wine prefix on the machine.

x11vnc moved from 127.0.0.1:5907 to a unix socket in a mode-0700 directory.
-nopw was only ever safe because of the machine, not the platform: any local
account could open that port and take the display. A fixed port is squattable
too. There is now no listener to authenticate against.

The wine prefix is confined to C:. This is the one that mattered. The platform
classified Minesweeper as kind:'execution' — never grantable — because a fresh
prefix maps the whole filesystem to Z: and symlinks the profile into the
owner's real home, and the game has Open and Save dialogs. A plugin permission
has no kind and is always grantable, so extracting it without this would have
converted a sealed capability into a file picker running as the owner.
Re-asserted on every start, because wine restores z: whenever it updates a
prefix.

Not solved: one display and one game, so two grantees share a board.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-23 17:43:17 +00:00