The app container had a fixed height: 100dvh with overflow: hidden, which
is correct for the game layout but cut off the start screen on small
viewports (mobile phones) — map cards, difficulty selection, and the help
section were unreachable with no way to scroll.
Changes:
- App.vue: add a "scrollable" class (overflow-y: auto) to the app container
when the screen is "menu" or "lobby"; the in-game layout keeps the fixed
locked viewport
- LobbyScreen.vue: change .lobby height: 100dvh to min-height: 100dvh so a
tall lobby card grows instead of being clipped by flex centering on small
screens
Verified in a real browser at 390x700 (mobile viewport): the menu content
overflows (2261px vs 700px viewport), .app reports overflow-y: auto and is
scrollable, and after a simulated swipe the help section at the bottom
becomes visible (scrollTop 900 of max 1561).
1Password showed "No items to display" on the signup password field
because login and registration shared a single form whose password input
switched autocomplete between current-password and new-password, and the
display-name field was dynamically added via v-if. Per 1Password compatible
website design, unrelated flows must be separate forms with stable fields.
Changes:
- Split into two distinct <form> elements: login (autocomplete
current-password) and signup (autocomplete new-password), each with
unique field ids so 1Password does not cache the field as a login field
- Add passwordrules, minlength=8, and maxlength=128 to the signup password
input so 1Password can generate a password matching the server rules
(min 8 chars)
- Remove the dynamic autocomplete computed and the v-if display-name field;
each form now has a static, complete field set
- Add .fields form styling (flex column, gap) now that the forms sit inside
the card container
Password managers (1Password, Bitwarden, …) could not reliably detect or
fill the auth fields because the inputs were loose labels inside a div with
no real form, no name attributes, and a static autocomplete hint.
Changes:
- Wrap the auth card in a real <form @submit.prevent> so password managers
recognize the credential form and its submit flow
- Add id/name attributes to all fields (username, displayName, password)
- Use a dynamic autocomplete hint on the password field:
current-password for login (autofill) and new-password for registration
(offer a generated strong password)
- Set autocomplete="nickname" on the display-name field so it is not
mistaken for a username or password field
- Mark the close, tab-switch, and OIDC buttons as type="button" so they no
longer default to type="submit" and trigger form submission
- Make the primary action a type="submit" button so Enter submits natively
- Add margin: 0 to .card since it is now a form element
The bind-mount permission fix only takes effect after a rebuild, so a
stale image keeps failing with the opaque "unable to open database file".
This hardens both sides so any remaining failure is self-explanatory:
- docker-entrypoint.sh: fall back to chmod 777 when chown is unsupported
(network/9p mounts), and fall back to running as root when su-exec is
unavailable, so the data dir is always writable on any filesystem.
- server/db.mjs: wrap the DatabaseSync open in a try/catch and, on
failure, report the exact path, whether the directory is writable, and
the process UID instead of the bare SQLite error.
Verified with a real container: a normal bind mount serves /health and
creates trxtd.db; a read-only mount now prints the directory-permission
diagnostic instead of the raw SQLite error.
The container ran as the unprivileged "node" user, but bind-mounted host
directories (e.g. ./data:/app/data in docker-compose or an Unraid appdata
folder) are mounted with root ownership. The node user therefore could not
create trxtd.db in /app/data, failing at startup with:
Error: unable to open database file
at file:///app/server/db.mjs:12
Fix:
- Add docker-entrypoint.sh that mkdir/chown /app/data to node:node on
container start, then drops privileges back to node via su-exec
- Install su-exec in the runtime stage (apk add --no-cache su-exec)
- Remove USER node so the entrypoint runs as root and can fix ownership,
with privilege dropping handled inside the entrypoint instead
- Wire the script up as ENTRYPOINT, keeping CMD as the app itself
The mount path itself is unchanged (/app/data, host ./data). Verified with
a real bind mount: /health responds ok and trxtd.db/-shm/-wal are created
in the mounted directory.
- Add a toggle on the picture-in-picture window to swap the big board
with the mini view of the opponent's field
- Color-coded frame and badge identify whose board is currently shown
(blue = player 1, orange = player 2)
- Block building and rushing while spectating the opponent's board;
the PiP then shows your own field instead
- Clarify duel-mode descriptions in lobby and start screen, add hints
in the multiplayer bar
- Replace previous announcement banners so only one shows at a time
- Add CSP, frame and referrer headers for served static files
- Enforce WebSocket origin check to prevent cross-site hijacking
- Trust X-Forwarded-For only when the peer is from a private proxy network
- Limit concurrent connections (500 total / 20 per IP) and rooms (300)
- Add ALLOWED_ORIGINS env var for additional WebSocket origins
- Document reverse proxy setup (NPM/NPMplus) in README
- Add scripts/security-test.mjs to verify origin and limit behavior
- Add Dockerfile (multi-stage build, unprivileged user, healthcheck),
docker-compose.yml and .dockerignore
- Server now serves static dist/ frontend and WebSocket relay on a
single port (PORT, default 3001) with path-traversal protection,
immutable asset caching and SPA fallback
- Client connects via same origin in production; dedicated ws port is
only used for vite dev/preview
- Document Docker usage, env vars and manual production mode in README