master
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a51c75cc6c |
a control for when the input state goes wrong
`POST /reset-input` and a "clicks acting wrong?" button in the panel's status line. It releases every modifier and mouse button the display thinks is held. Named for the SYMPTOM rather than the mechanism, because nobody experiencing this knows a modifier is stuck — they know their clicks stopped doing the right thing. ── both sides, and that ordering matters ── Releasing on the server alone leaves the panel's own button mask stale and the next mouse move re-asserts it, so the fix would appear not to work. Local state is cleared first, a pointer event with an empty mask says so, and only then does the server let go. ── a fixed list, not a query ── `xinput --query-state` would report exactly what is held, which is better. It is also a seventh host binary to declare and to require, for a repair. Releasing a key that was not pressed is a no-op, so the fixed list costs nothing. Modifiers and buttons only: a stuck letter types, which is obvious and harmless; a stuck modifier silently changes what every click means. ── not guarded like the others ── Every other command refuses while a dialog is open, because its clicks would fall through onto the board. This one clicks nothing, and refusing a repair in the state that most needs it would be the wrong way round. Verified by stranding `Shift_L` on the display with `xdotool keydown`, hitting the route, and watching `xinput --query-state` go from one key down to none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b |
||
|
|
5d62668883 |
difficulty and replay panels in the left column
Top panel: Expert / Intermediate / Beginner. Bottom: that difficulty's saved
games, best first, click to play one on the board.
── the selection lives in the URL ──
`/minesweeper/expert`, not a channel and not panel state. It survives a
refresh, it can be linked, and the two panels never pass anything between
themselves — both read `useParams`. The plugin section route already existed;
nothing had used it.
The side effect hangs off the URL too: an effect on the selected level posts
the difficulty, so arriving at `/minesweeper/expert` by click, deep link or
back button all leave the game on expert. `POST /difficulty` verifies and is
idempotent, so a repeat changes nothing. One `<Navigate replace>` after the
hooks canonicalises both the bare route and a bogus section, and it waits for
`/status` so the URL lands on what the game IS rather than a guess — which is
why `difficulty` is now part of that payload.
Levels are NavLinks so cmd-click works and the active style comes from the
router. Replay rows are buttons, because they do not navigate anywhere, they
make the game do something. That is the line navigation-audit.md draws.
── two things on the server ──
`GET /replays` reads the directory rather than any index, because the filename
IS the metadata — Minesweeper X names a recording `<Lvl> <secs> - <date>.mvr`
and keeps no catalogue. A name that does not parse is skipped rather than
guessed at. Ordering is the server's: a loss is never "best" however short it
was, so wins rank by time and losses fall to the end, still watchable.
`POST /replay` drives Extras > Play Recording... and TYPES the filename into a
file dialog, which is why it only accepts a name `isKnownReplay` just matched
against the directory. An unchecked client string reaching `xdotool type` can
carry a path, a wildcard, or a newline that submits something else. Verified by
the dialog closing, not by the window size — a beginner replay leaves the
window beginner-sized, which is also what nothing-happened looks like.
Verified end to end against the running session: the expert list comes back
ranked 58.63 through 82.06 with losses after, playing 58.63 starts on the
board, and `{"file":"../../../etc/passwd"}` is a 404.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
|
||
|
|
977448868c |
POST /difficulty — set the board from outside the game
First command in what will presumably be several, so the shape matters more
than the feature: name the OUTCOME, verify it against the world, and report
what the game now IS rather than that a click was sent.
POST /api/minesweeper/difficulty { "level": "expert" }
-> { "ok": true, "level": "expert", "changed": true }
The proxy forwards <prefix>/*, so this needed no platform-side change.
── clicks, not keys, and that was measured ──
The obvious mechanism is `alt+g` then the underlined letter. It does not work
reliably: the same sequence gave a different difficulty depending on what was
selected before, and `alt+g e` landed on Intermediate more than once. Arrow
navigation was no better. A menu keystroke is a SEQUENCE, so it depends on
timing and on where the highlight already was; a click is one event that names
its target. Six scrambled clicks in a row hit six correct items.
There is a second reason keys are wrong here. With no window manager, X input
focus is PointerRoot — keys go to whatever the pointer is over — so a sequence
that RESIZES the window can move it out from under the pointer and silently
drop the rest of its own keystrokes. That is exactly what the first attempt
did, and it looked like a mapping problem.
── two things that make it a command rather than a click ──
`withStableWindow` holds off the re-centring watcher for the duration. A menu
interaction is two clicks ~450ms apart and the watcher ticks every second, so
it WILL move the window between them, and the second click then lands where
the item used to be — which on this game is the board. A stray click on a live
board reveals a cell and can lose the game.
And the check is the window's SIZE, not that the click was sent: 152x214,
280x342, 504x342 are distinct, so the result is verifiable. That is what makes
retrying safe, and what makes a repeat a no-op (`changed: false`) rather than a
second difficulty change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
|
||
|
|
e6cec953e4 |
a fixed stage for the game, and close the VNC port nobody meant to open
── the stage ── The framebuffer is now the picture. 800x600 whatever the difficulty, the game centred in it by the sidecar, and the panel draws it entire — scaled to fit, centred by its flex parent, never cropped. Changing difficulty moves the board instead of resizing the panel. What this replaces parked the window at the origin and had the client crop to the window's size, polled every four seconds. Two things wrong with it. The panel resized under the player mid-game; and wine puts the window at 3,29 rather than 0,0, so cropping from the origin showed a black strip on top and cut a row of cells off the bottom — the "expert gets cut off" report. Centring runs on a one-second timer because nothing announces a resize: not RFB, which keeps the framebuffer, and not X without a window manager to listen through. One chained `xdotool search … getwindowgeometry` per tick, one process, and a `windowmove` only when the numbers actually changed. The client loses the geometry poll, the offscreen canvas and the crop with it. ── the port ── x11vnc was listening on 0.0.0.0:5900 and [::]:5900 with -nopw. Every account on this machine, every device on the tailnet and everything on the LAN could open it and take the display — which is precisely the hole the 5907 version was replaced to close, reopened wider and documented as impossible. `-unixsock` ADDS a unix socket. It does not remove the TCP one. Three flags do: -rfbport 0 for IPv4, -rfbportv6 0 for IPv6, -noipv6 for outgoing. `-rfbport 0` alone leaves [::]:5900 up, which is how this nearly shipped half-fixed. Found with `ss -lntp` on the running process, and that is the only check that was right about it at any point. Running the same argv by hand showed no IPv6 listener — most likely because the PM2 session still held [::]:5900 and the hand-run copy failed to bind it silently. Comments in session.ts and index.ts that asserted there was no port are corrected rather than deleted. Verified on the live socket: the streamed framebuffer is 447k teal pixels around a 152x214 window at 324,193 — dead centre — and window grey arrives as c0 c0 c0, no TCP listener for the x11vnc pid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b |
||
|
|
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 |