efd91c3e8e1eb5784ab1bdf381e9d24b4575a274
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
dd4d1b97c8 |
feat: serve the BYOS device API
The panel now pairs, fetches its image and files its logs against this application rather than against the TRMNL cloud. Four endpoints: /api/setup issues a token on first contact, /api/display hands back an image and a wake interval, /api/log stores firmware diagnostics, and /api/device/image/<hash> serves the bytes. The wake interval is where freshness and battery are traded off. In BYOS nothing can be pushed: the device sleeps, wakes, asks and sleeps again. So the interval is short while the shop trades and long overnight, and it is shortened further whenever a change of state falls inside it — the door opening in twenty minutes means waking in twenty-one, whatever the base interval says. The image filename is the hash of its own bytes. The firmware skips the redraw when the name is unchanged, which is the whole battery strategy, and the URL is immutable, unguessable and safe to cache forever. Two integration tests pin this: unchanged data must yield the same filename and store one row, changed hours must yield a different one. MAC addresses are normalised before use. They are a primary key here, and firmwares are inconsistent about case and separators; without this a panel could register twice by capitalising itself differently. Header names are read in both the hyphen and underscore spellings for the same reason — the TRMNL docs and the Seeed sources disagree, and being liberal costs nothing while being wrong costs a blank shop window. Pairing is deliberately made to survive a rendering failure. The token is issued once and only its digest is kept, so a device stranded by a failed response would be registered yet hold no credential, and unable to register again. The welcome image is worth far less than that. This was found by running the flow, not by reading it. satori, yoga and harfbuzz are marked external: bundling rewrites the relative path satori uses to load its WebAssembly, and the renderer dies on a missing hb.wasm. The integration tests run against a real Postgres, in CI too. Mocking Prisma here would only prove the mock works. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd |
||
|
|
7a1e833971 |
feat: add the schedule resolution engine with its test table
This is the business core, written before any UI as the spec requires. It is pure: no I/O, no database, no clock of its own. Everything arrives in a ScheduleContext, so every rule below is exhaustively testable. The engine works on civil (wall-clock) values rather than instants. A slot that runs 10:00-18:30 runs 10:00-18:30 on the nights the clocks change too, and day arithmetic goes through UTC, which has no daylight saving. That turns the March and October switches from edge cases into non-events, and the tests pin both of them. Priority order, highest first: a dated exception, a vacation period, a public holiday the shop closes for, the reference week, then closed. Exceptions are badged by their stated reason rather than by who created them, so a holiday imported by the sync shows as a holiday in the UI. The search for the next opening is bounded to fourteen days. A shop that is closed forever must not make the server spin; past the horizon the screen simply says nothing about reopening. Both the never-open week and the beyond-the-horizon reopening are covered. "Soon" is strictly under thirty minutes, so a change exactly half an hour away still reads as plain OPEN or CLOSED. 55 tests; lib/schedule sits at 97% statements and 93% branches against an 85% floor. The thresholds were verified to actually fail the build before being committed. 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 |