Commit Graph
3 Commits
Author SHA1 Message Date
vliaudatandClaude Opus 5 7d23b14ad8 feat: add the one-off schedule changes page
The page opens on the gesture the shop actually makes: changing today's
hours from a phone, behind the counter. "Closed today", "opens later at"
and "closes earlier at" are one tap plus a time, and each shows the
hours it would produce before it is applied.

The derivation is the delicate part and is pure and tested. Opening at
14:00 drops a 10:00-13:00 morning rather than keeping it, and trims the
slot the new time falls inside rather than dropping it. Closing early is
the mirror. Both are computed against what the day would normally be,
ignoring any exception already recorded, since that is what "late" is
late relative to.

The upcoming list is built by resolving each of the next sixty days, not
by reading the exception table. Vacations and public holidays are never
materialised as rows, so the table alone would quietly omit most of what
is actually in effect. Each entry is badged with the rule that produced
it, and only stored exceptions offer a delete.

Ranges are capped at 92 days and point at the holidays page beyond that:
a typo in a year field should produce an error, not thirty thousand rows.

Editing the French note clears the English one. A translation of text
that has changed is worse than no translation.

Three exports were removed from the actions module before committing:
every export in a 'use server' file becomes a publicly callable
endpoint, and those three had ended up unused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-20 19:11:28 +02:00
vliaudatandClaude Opus 5 eb23eb650b feat: edit the reference week from the admin
The first administration page, built for the actual use case: a phone
held in one hand behind the counter. One card per day, native time
inputs so the platform keyboard does the work, and the consequences
shown before the save rather than after — the footer names which days
are about to change, and the button stays disabled until something
actually has.

"Duplicate onto the other open days" leaves closed days closed. Someone
copying Tuesday's hours means "the days I open, I open like this", not
"open seven days a week".

Validation runs in the browser for the feedback and again in the action
before the write: the client is a convenience, not a guarantee, and this
is the schedule the shop window shows. A day being closed drops its
leftover slots rather than failing on them.

A save that changes nothing writes nothing — no rows, no audit entry,
and so no needless panel redraw. Reordering slots does not count as a
change. The audit diff stores one readable line per day in French, so
the log can be read without cross-referencing the schema.

The editing helpers are pure and tested, and the write path is tested
against a real database including the read-only refusal.

Test files now run sequentially: the integration files share one
database and each truncates it, so parallel files raced. The suite takes
six seconds; giving every file its own database would buy nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-20 18:57:46 +02:00
vliaudatandClaude Opus 5 ac6be97d9b feat: authenticate against Authentik with admin and viewer roles
Sign-in goes through Authentik over OIDC with PKCE. Verified against the
live provider: the discovery issuer matches the configured one exactly,
and the authorize redirect carries code_challenge_method=S256.

Sessions are JWTs with no database adapter, which keeps the module
usable from edge middleware and makes a sign-in cost no query. The
trade-off is stated in the code: the role travels in the token, so
removing someone from the admin group takes effect at the next sign-in
or when the eight-hour session expires, not instantly. Immediate
revocation would mean asking Authentik on every request, which is what
api_llm_loxi does and what this application deliberately does not — it
drives a shop window, not a fleet.

Group matching is trimmed and case-insensitive. Authentik group names
are case-sensitive, but a capitalisation mismatch between the group and
the environment variable locks the shop owner out silently, and that is
the worse of the two failures. An empty variable never promotes anyone.

Authorisation is enforced twice. The middleware covers every /admin page
and /api/admin route at the edge; a guard inside the handlers repeats
the check, because a matcher is a string, strings get edited, and a
route falling outside one should not be the same thing as a route with
no access control. The rule itself lives in its own framework-free
module so it can be tested directly. Unknown HTTP verbs count as writes:
new methods arrive locked.

Pages get a redirect to the sign-in screen, API routes get a status
code — a fetch that receives an HTML login page is a confusing way to
learn you are signed out. The device API stays outside the matcher, as
the panel cannot sign in and carries its own bearer token; this is
covered by a check that /api/display still answers 401 rather than
redirecting.

The audit diff compares values by their JSON form, so slot arrays and
dates compare by value rather than identity, and a save that changed
nothing writes no entry. Recording never throws: losing the trail is
bad, refusing the user's change because the trail could not be written
is worse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-20 18:26:47 +02:00