5f7aa951d3daca45020373c92d0e5e9c46017c50
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
185610ee61 |
feat: move the week down the right-hand side of the panel
A bottom strip gave each day a 114px column, and that width was what capped the type size. A side band gives 142px of usable width and a 68px row, so the hours go from 19pt to 22pt, the day labels from 20 to 23, and each day fits on one line — "Mar 11:00–18:00" reads at a glance where a column had to be read downwards. Right rather than left, for two reasons the renders make plain: on the left the logo is pushed into the middle and loses its place as a signature, and the eye meets the week before the day's status. OUVERT is what has to be read first from the pavement. The times stay whole. Only the spaces around the dash go, which is enough to hold 22pt without abbreviating anything: a shop's opening hour is not a detail to shorten. The idea came from the shop owner, and testing it surfaced a defect that had been in production all along. Rendering the worst case the data model allows — three periods in a day, none on a round hour — showed the day's hours wrapping, pushing the layout down, and the English line of the banner falling off the bottom edge. A panel does not scroll and does not reflow: what does not fit is lost. Type size is now fitted to the space available, with a floor below which it will not go, and nothing may leave its box. The width estimate is deliberately crude: measuring glyphs would mean loading the font in that module and would still be an estimate, since the renderer applies its own kerning. Erring small costs a point of type, erring large costs a broken screen. Three periods a day will reportedly never happen. The protection stays anyway — it costs nothing and removes the need to remember. `npm run screen:fonts` renders every arrangement and both alternative typefaces, including the worst case, because choosing type for a 1-bit panel is done by looking at it thresholded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd |
||
|
|
446021cd75 |
test: add the end-to-end suite, on desktop and on a phone
Twenty-six tests across two viewports, covering what the spec asks for: changing today's hours reaches the panel, a message survives the translation service being unavailable, a closure period closes the days it covers — plus the authorisation paths and the sign-out regression. They run against the standalone build served the way the container serves it, not `next start`, which refuses to work with standalone output anyway. The suite therefore exercises the artifact that ships rather than a second arrangement that could drift from it. Sign-in mints the session cookie Auth.js would have issued rather than driving Authentik. What is under test is the application's behaviour for a given role; the handshake itself is verified against the live provider separately, and standing up an identity provider per run would trade a lot of machinery for coverage of somebody else's code. The secret lives in one module imported by both the config and the fixtures — when it differed, every signed-in test failed at once while looking like an authorisation bug. Database access goes through plain SQL rather than the Prisma client, whose generated module format Playwright's loader and Next's bundler disagree about. That traded one problem for a subtler one: node-postgres parses a DATE column into a local-midnight Date, so reading it back shifted the day at UTC+2. Dates are read as text now. The mobile profile runs on Chromium: WebKit needs system packages only root can install, and a suite nobody can run locally is a suite nobody runs. The config says how to switch to the real engine. Two real defects surfaced, both found by the tests rather than by reading. The seven "Ouvert" checkboxes on the hours page were indistinguishable to a screen reader; each now names its day. And on a phone the signed-in address appeared nowhere at all — the header hides it to save room — so nobody could tell which account was about to sign an audit entry on a device the shop shares. It is on the dashboard now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd |
||
|
|
f89e4690ba |
feat: package the application for Docker, with deployment docs
Multi-stage build on node:22-alpine, standalone output, non-root user, healthcheck on /api/health, and migrations applied by the entrypoint before the first request. A failed migration stops the container rather than serving an inconsistent database. Getting the Prisma CLI into the runtime image took three attempts and the reasoning is recorded in the Dockerfile. Copying it out of the build stage leaves its transitive dependencies behind; patching them in one at a time is a losing game. It now gets its own stage and its own tree, with the schema and prisma.config.ts beside it, and the entrypoint runs from there so every import resolves locally. The version is read from our own package.json so it cannot drift from the generated client. Two things had to change to build without a database, which a build container rightly does not have. prisma.config.ts no longer reads the URL through prisma's env() helper, which throws on a missing variable even for `generate`. And lib/db.ts creates the client on first use rather than on import: Next imports every route module while collecting page data, so a module that threw on import failed the build with an error naming whichever route was analysed first, which says nothing useful. The failure now lands on the first query, where it belongs. Verified by running the image against a real database: migrations applied, cron scheduled in Europe/Zurich, a device paired, and the panel image served as a genuine 1-bit 800x480 BMP — so satori, resvg and the vendored fonts all work on musl. The image hash came out identical to the one produced on the glibc host, which is the reproducibility the vendored fonts were for. The production overlay publishes through an existing Traefik, drops the host port, mounts the filesystem read-only, and adds a nightly dump kept for a fortnight. README and DEPLOY are in French and cover what actually bites: the panel receives nothing and only updates when it wakes; the issuer must match to the character; the captive portal URL takes no trailing slash; a rollback across a migration needs the dump, because Prisma does not undo one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd |
||
|
|
fccccbd118 |
feat: render the panel image as a 1-bit 800x480 BMP or PNG
With the move to BYOS there is no TRMNL cloud to interpret a Liquid template, so the application draws the screen itself. The pipeline is satori (flexbox to SVG) then resvg (SVG to pixels) then a hand-written 1-bit encoder. Chromium was the alternative and was rejected: half a gigabyte and a real memory appetite in the runtime image, for a screen of eight blocks. Playwright still handles the E2E tests, in its own pinned image, never in the application one. The payoff is testability. The layout is asserted on the element tree and the geometry on the SVG text, so a rendering regression shows up as a readable diff instead of a pixel comparison. The BMP and PNG encoders are verified field by field against their specifications, including an independent CRC-32 for the PNG: a device rejecting a malformed image is expensive to debug from a shop window. Two properties are pinned because the battery depends on them: the same payload must produce byte-identical output, and changed hours must produce different output. The filename handed to the device is a hash of these bytes, and the firmware skips the redraw when it is unchanged. The fonts are vendored into public/fonts and the logo into public/brand, both committed. Rendering must not depend on an install tree, a CDN or the network, or the bytes drift and the panel wakes for nothing. `npm run screen:preview` writes a real 1-bit image plus a magnified view, which is where clipping and thin strokes give themselves up. That is how the week strip was caught clipping and condensed. `npm run brand` rebuilds the assets: it locates the wordmark band in the shop logo rather than hard-coding offsets a future revision would break, and thresholds it with the panel's own encoder. sharp is a devDependency used only there; nothing at runtime needs it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd |
||
|
|
42ce09100e |
feat: add Prisma schema, initial migration and idempotent seed
Model the whole domain: settings, the reference week, dated exceptions, vacation periods, the public-holiday cache, display messages, the translation cache, the audit log, and the device tables the BYOS server needs (Device, DeviceLog, SyncState). Two modelling decisions are load-bearing and documented in the schema: - Opening times are wall-clock "HH:mm" strings in the shop timezone, never instants. Nothing is stored in UTC, which turns the March and October daylight-saving switches into non-events instead of edge cases. - VacationPeriod is a source of truth, never expanded into ScheduleException rows. The resolver reads it directly at priority rank 2, so a holiday sync can never overwrite a manual exception and editing a period leaves no orphans behind. Device access tokens are stored only as SHA-256 digests, and the image filename column holds a content hash: the firmware skips the redraw when the filename is unchanged, which is where the battery life comes from. Prisma 7 no longer accepts the connection URL in the schema file, so it moves to prisma.config.ts with the pg driver adapter. The dev Postgres service lands here rather than with the rest of the Docker work, because the migration needs a database to run against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd |
||
|
|
a9809f83ca |
chore: scaffold Next.js 16 admin app with ITA ITO design tokens
Set up the project skeleton for the ITA ITO opening-hours display admin: Next.js 16 (App Router) with TypeScript in strict mode, Tailwind CSS 4, Vitest, ESLint and a blocking CI workflow. The design tokens are copied verbatim from the model_ita_ito project (palette, Inter Variable + Source Serif 4, radii, dark theme) so the two applications look like one family, as required by the spec. ESLint is pinned to v9: eslint-config-next bundles a react plugin that crashes on ESLint 10. The typed `consistent-type-imports` rule is left out because `verbatimModuleSyntax` already enforces the same discipline at compile time, without the cost of typed linting across the repo. PLAN.md records the agreed architecture, including the decisions that depart from the original spec — most importantly the move from BYOD to a self-hosted BYOS server, which removes the TRMNL private plugin, the Liquid template and the webhook entirely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd |