Trimming the stage alone does nothing, and that is worth writing down: the
scale is snapped, so at 3x the game is 1512 device pixels wide whether the
stage is 560 or 512. Shrinking the surround only pays when it crosses a step.
So both halves land together.
── the stage ──
560x400 down to 512x352, which is expert (504x342, measured) plus four pixels
a side. There is nothing left to give — intermediate is 342 tall too, so
anything shorter clips a board people actually play.
── half steps from 2x ──
Not a retreat from whole-number scaling. The unit that has to stay regular is
the CELL, not the pixel: this is a 16px grid, so at 3.5x a cell is exactly 56
screen pixels — as even as 3x or 4x — and only sub-cell pixels alternate
between three and four. Below 2x that alternation is a third of a pixel rather
than a seventh, so whole numbers still apply there. The case that started all
this, a fractional devicePixelRatio at nominal 1:1, put the CELL boundaries at
26.4px, which is a different thing entirely.
Together, on a 1800x1400 device-pixel panel: 3.0x becomes 3.5x, and the game
goes from 1512 to 1764 device pixels wide.
Known cost, and it is in the README: the Extras > Auto Options dialog is 328
wide and opens 25px right of the game, so it needs a 554-wide stage to fit
whole from beginner. It cannot be closed from the panel anyway — no window
manager, no title bar, no OK button — so that is the thing to fix before this
number is revisited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
Two halves of the same problem: the picture was sharp everywhere and wrong
overall.
── whole-number scaling ──
The canvas is now sized to the largest WHOLE number of device pixels per
source pixel that fits the panel, measured with a ResizeObserver and the
device pixel ratio. Below 1x it takes a unit fraction instead, so the rows
that survive are evenly spaced rather than dropped in clumps.
This is a 16px-per-cell bitmap from 1995. At a fractional scale one cell gets
three screen pixels and its neighbour gets two, the grid goes uneven and the
digits wobble — which reads as "not quite right" rather than as blur, and
`image-rendering: pixelated` does not help because unevenness is not blur.
A fractional devicePixelRatio produces exactly this at what looks like 1:1.
It is also allowed to scale UP past the framebuffer's own size, which was the
other half of it: `max-w-full` only ever shrinks, so on a wide panel the
canvas sat at its intrinsic size using a fraction of the pixels available.
── a smaller stage ──
800x600 down to 560x400. The stage is what gets scaled to fill the panel, so
every pixel of empty surround is panel the game does not get, and the whole-
number steps are far apart: in an 1866-device-pixel panel, 800 wide lands on
2x and 560 wide lands on 3x. Same panel, half again as much game.
560x400 is expert (504x342, measured on this machine) plus a margin. A custom
board bigger than the stage is now clipped at the right and bottom rather than
shrunk; `centre` clamps at zero so the menu bar stays reachable. Widen the two
constants if that turns out to be the wrong trade.
Verified: beginner centres at 204,93 on the new stage, which is exact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
── 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
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