4 KiB
Web Dashboard layout V1
Status: WEB_DASHBOARD_LAYOUT_V1=PASS/FROZEN
File contract
The canonical UI preference is
$XDG_CONFIG_HOME/trainlog/web/dashboard-layout-v1.json, falling back to
$HOME/.config/trainlog/web/dashboard-layout-v1.json. Trainlog-owned
directories are mode 0700 and the regular file is mode 0600. It is not SQLite,
training data, an exchange format or synchronization input.
The strict document has exactly format, version, revision, columns and
tiles. Format is trainlog-dashboard-layout, version is 1, columns is 12,
and all seven stable Grid V1 IDs occur exactly once. Tiles contain only
id,x,y,width,height; responsive projections and translated titles never
persist. The canonical constraint/default table is
contracts/dashboard-layout-v1.json; TypeScript consumes it directly and a
Meson contract test rejects C divergence.
Revision zero denotes no valid persisted preference. A successful PUT stores
the expected revision plus one. The maximum is JavaScript's safe integer,
9,007,199,254,740,991. Coordinates are integral, y <= 200, dimensions obey
the frozen per-tile bounds, all tiles fit 12 columns and overlap is forbidden.
Parser and boundedness
Trainlog uses system yyjson 0.13.0 (MIT), selected for strict typed C parsing,
small surface and pkg-config/Meson integration. HTTP bodies and files are both
limited to 4 KiB. Only the exact four-level schema is accepted; unexpected
keys, types, array sizes and unsupported versions are rejected. IDs fit a
32-byte bounded buffer and the tile count is exactly seven. No general JSON
framework was introduced.
Reads use O_NOFOLLOW|O_CLOEXEC|O_NONBLOCK, fstat, regular-file and size
checks. A missing file is normal. An invalid, oversized, non-regular or symlink
preference yields the complete default with source invalid_persisted; it is
not silently removed or replaced, and the server emits one bounded diagnostic.
Atomic mutation
PUT validates before filesystem mutation, creates a same-directory mkstemp,
sets 0600 and close-on-exec, writes fully, fsyncs, closes, atomically renames,
then attempts directory fsync. Failures before rename unlink the temporary and
leave the prior file untouched. After rename the visible commit is authoritative
even if directory fsync cannot prove crash durability, so clients are never
invited to retry against a stale revision.
DELETE checks the same revision then unlinks only the preference and attempts directory fsync. It never touches SQLite. Saving the default through PUT is valid and deliberate; frontend Reset remains a draft until Save.
HTTP and mutation security
GET /api/v1/dashboard-layout returns default, persisted or invalid-persisted
source, an ETag containing the revision and an ephemeral CSRF token header.
PUT and DELETE require the loaded ETag in If-Match; stale revisions return
412. Missing/malformed preconditions return 428.
Mutations additionally require an exact Origin of either the direct active
loopback URL or http://trainlog.perf, plus the 256-bit startup-random token in
X-Trainlog-CSRF-Token. The token is never persisted, logged or placed in a
URL. nginx may keep rewriting backend Host to the already accepted
127.0.0.1:8080; Origin remains the browser origin and requires no proxy
change. Host validation, loopback binding, no-CORS behavior and request bounds
remain unchanged.
Frontend lifecycle
The Dashboard withholds the grid behind a discreet loading state until GET
finishes, avoiding default-to-persisted flashing. Network failure falls back to
the default with an accessible warning and disables Save because no safe
revision/token was loaded. Entry into edit mode snapshots the loaded canon;
Cancel is local, Reset changes only the draft, and Save validates then PUTs.
Success adopts the server response and revision. A 412 keeps editing open,
announces the conflict and exposes Recharger l'agencement.
Only the 12-column desktop canon is sent. Six-column/tablet and one-column/phone projections remain derived and cannot trigger mutation.