Tightening the gaps bought letters a third taller at the same width, which helped the hairlines — but it changed the logotype's character, and the shop prefers the drawing as it was designed. The logo returns to 260x36 with its spacing intact. The threshold fix that made the A's diagonal survive stays: that was a separate problem and it is still solved. lib/brand/tighten.ts and `npm run brand:variants` are kept rather than deleted. Both are tested, the variants script is also how the threshold itself was chosen, and a spacing decision is one to revisit by looking at renders rather than by imagining them. The logo's rendered size is still read from the PNG header rather than copied into the renderer, which is what made this reversal a one-line change with nothing to keep in sync. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
Horaires ITA ITO
Panneau d'administration des horaires d'ouverture de la boutique ITA ITO, et serveur d'affichage pour un écran e-ink TRMNL 7,5" (kit DIY Seeed) posé en vitrine.
Changer un horaire depuis un téléphone, debout dans la boutique, en deux clics — et que l'écran suive tout seul, jours fériés genevois et message bilingue compris.
Écran e-ink (ESP32, Wi-Fi boutique)
│ GET /api/setup · GET /api/display · POST /api/log
│ GET /api/device/image/<hash>.bmp
▼
┌──────────────────────────────────────────────┐
│ Next.js 16 + PostgreSQL 16 │
│ horaires.ita-ito.com │
└──────────────────────────────────────────────┘
▲ │
│ OIDC ├─► api.loxi.ch (traduction FR→EN)
Navigateur admin └─► openholidaysapi.org (jours fériés CH-GE)
auth.loxi.ch
Ce qu'il faut savoir avant tout
L'écran ne reçoit rien. Il dort sur batterie, se réveille, demande quoi afficher, et se rendort. On ne peut pas lui pousser une mise à jour : le seul levier est la durée de son sommeil, réglable dans Paramètres → Écran. Une modification apparaît donc au réveil suivant, pas immédiatement. C'est la nature du BYOS, pas une limitation de cette application.
Le nom du fichier image est le hash de son contenu. Si rien n'a changé,
l'écran reconnaît le nom, ne redessine pas, et économise sa batterie. C'est
pourquoi le rendu doit rester reproductible à l'octet près — d'où les polices
et le logo versionnés dans public/.
Prérequis
- Docker et Docker Compose
- Node.js ≥ 20.9 (seulement pour le développement hors conteneur)
- Une application OIDC dans Authentik (voir plus bas)
- Un écran TRMNL 7,5" dont le portail captif accepte un serveur personnalisé
Installation locale
cp .env.example .env
sed -i "s|^AUTH_SECRET=.*|AUTH_SECRET=$(openssl rand -base64 33)|" .env
# Renseignez ensuite POSTGRES_PASSWORD, AUTH_AUTHENTIK_*, TRANSLATION_API_KEY.
docker compose up -d db --wait # PostgreSQL seul
npm install
npx prisma migrate deploy
npm run seed # horaires de démonstration, idempotent
npm run dev # http://localhost:3010
Le port 3010 n'est pas un caprice : 3000 est déjà pris par une autre pile sur la machine de développement.
Pour faire tourner l'application elle aussi en conteneur :
docker compose up -d --build --wait
Commandes utiles
| Commande | Effet |
|---|---|
npm run dev |
serveur de développement sur 3010 |
npm test |
toute la suite (unitaire + intégration) |
npm run coverage |
idem, avec les seuils de couverture |
npm run lint / npm run typecheck |
ESLint / TypeScript strict |
npm run screen:preview |
rend l'écran dans .preview/, sans appareil |
npm run brand |
régénère le logo depuis le CDN de la boutique |
npm run holidays:sync |
synchronise les jours fériés maintenant |
npm run translate:test "texte" |
vérifie le service de traduction |
Les tests d'intégration ont besoin d'une base à eux — TEST_DATABASE_URL. Ils
la vident entièrement, donc elle ne doit jamais désigner une base qui contient
quoi que ce soit d'utile. Sans cette variable, ils se désactivent plutôt que de
détruire quelque chose.
docker compose exec db psql -U horaires -d postgres -c "create database horaires_test owner horaires;"
DATABASE_URL="$TEST_DATABASE_URL" npx prisma migrate deploy
Configuration d'Authentik
-
Groupe — créez
horaires-adminset ajoutez-y les personnes qui peuvent modifier les horaires. Ne liez pas ce groupe à l'application : seuls les administrateurs pourraient alors se connecter, et le rôle lecture seule deviendrait inatteignable. Pour restreindre l'accès, créez un groupe plus large et liez celui-là. -
Provider — OAuth2/OpenID Provider :
Champ Valeur Client type Confidential Signing Key n'importe quel certificat — obligatoire Redirect URIs (Strict) https://horaires.ita-ito.com/api/auth/callback/authentikhttp://localhost:3010/api/auth/callback/authentikScopes openid,email,profile(les valeurs par défaut)Subject mode Based on the User's Email Include claims in id_token activé Le claim
groupsarrive via le scopeprofile: aucun mapping à créer. Rien à activer pour le PKCE, Authentik annonceS256et Auth.js l'utilise seul. -
Application — slug
horaires-ita-ito, liée au provider. Le slug détermine l'issuer, qui doit correspondre au caractère près, barre oblique finale comprise. -
Vérification, avant même de lancer l'application :
curl -s https://auth.loxi.ch/application/o/horaires-ita-ito/.well-known/openid-configuration | jq .issuerLa valeur doit être identique à
AUTH_AUTHENTIK_ISSUER. C'est la première cause d'échec de connexion, et elle se diagnostique mal depuis l'application.
Le rôle est fixé à la connexion. Retirer quelqu'un du groupe ne coupe pas sa session en cours : l'effet arrive à la reconnexion, ou au bout de huit heures. Si un compte apparaît en lecture seule alors qu'il ne devrait pas, le tableau de bord affiche les groupes réellement reçus et celui qui était attendu.
Appairage de l'écran
Aucun reflashage. Le firmware d'origine suffit.
- Maintenez le bouton au dos de l'écran 5 secondes : il ouvre un réseau
Wi-Fi nommé
TRMNL. - Connectez-vous-y depuis un téléphone ; le portail s'ouvre tout seul.
- Donnez le Wi-Fi de la boutique, puis Advanced → Custom Server → Yes.
- Saisissez
https://horaires.ita-ito.com— sans barre oblique finale. L'URL exacte est rappelée dans Paramètres → Appareils. - Validez. L'écran s'enregistre seul au premier appel et affiche les horaires.
Il apparaît ensuite dans Paramètres → Appareils avec son identifiant, sa dernière visite, son firmware et sa batterie.
Si Custom Server n'apparaît pas dans le portail, le firmware livré avec le kit est une version qui ne l'expose pas. Il faut alors flasher le firmware TRMNL officiel — ce qui sort du cadre de ce dépôt. Vérifiez ce point avant de déployer quoi que ce soit.
Mise en production
Voir DEPLOY.md : première installation, mises à jour, sauvegardes, restauration et retour arrière.
Dépannage
| Symptôme | Cause la plus probable |
|---|---|
| Connexion refusée en boucle | AUTH_AUTHENTIK_ISSUER ne correspond pas exactement à l'issuer annoncé |
| « Compte en lecture seule » | le compte n'est pas dans AUTHENTIK_ADMIN_GROUP ; le tableau de bord affiche les groupes reçus |
| L'écran reste blanc | il n'a pas encore atteint le serveur : vérifiez l'URL du portail captif, sans barre oblique finale |
| L'écran n'a pas suivi une modification | normal jusqu'à son prochain réveil ; le tableau de bord indique quand il est attendu |
| « La traduction a échoué » | TRANSLATION_API_KEY absente ou révoquée — npm run translate:test le dit précisément |
| Jours fériés absents | lancez npm run holidays:sync ; la page Fériés affiche la dernière synchro réussie |
| Aperçu d'écran en erreur | le rendu a besoin de public/fonts et public/brand ; ils sont versionnés, vérifiez qu'ils sont présents |
Licence
Propriétaire. Voir LICENSE.