No description
  • TypeScript 37.2%
  • Svelte 27.1%
  • JavaScript 25.9%
  • CSS 4.9%
  • Just 1.7%
  • Other 3.2%
Find a file
thermo 57e724c2b1
All checks were successful
Build & deploy yaderive / build (push) Successful in 40s
Build & deploy yaderive / deploy (push) Successful in 4s
sign: at zero days, the plates watch you back
A zero is already the shape of an eye, and zero days is the canonical
state of the joke — so at 00 the two plates become a pair of eyes that
follow the pointer. Only at 00: from 01 they are digits again.

Each eye keeps EXACTLY a digit's box. An invisible "0" holds the metrics
and the SVG sits on top, so the sign does not change width between 00 and
01 and no size has to be hand-matched at each breakpoint.

Three things the idea needed before it could ship.

A desk screen has no pointer at all, and /panneau/<id> is where this will
mostly be seen. Without an idle behaviour the eyes would sit dead centre
forever, which reads as broken rather than funny — so after four seconds
without a pointer they start wandering, a new target every couple of
seconds, eased far more slowly than when they are tracking.

The eyes do NOT take currentColor, unlike every other colour in the app.
A digit inverts with the theme and stays legible either way; an eye whose
white turns black stops being an eye. Those two literals are the only
hardcoded colours here, and they hold on both plates because the plate is
a dark recess in both themes.

And prefers-reduced-motion stops everything — no tracking, no blinking,
eyes open and still. Someone who asked for less motion did not ask for a
pair of eyes to stare at them.

The blink is pure CSS, with slightly different periods per eye (7.3s and
7.9s) and an offset, so they never blink like a metronome.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 11:42:28 +02:00
.forgejo/workflows yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
k8s yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
server yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
src sign: at zero days, the plates watch you back 2026-09-22 11:42:28 +02:00
static yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
.dockerignore yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
.envrc yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
.gitignore yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
.npmrc yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
Dockerfile yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
flake.lock yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
flake.nix yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
justfile yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
package-lock.json yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
package.json yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
README.md docs: write down the change kinds and why each one exists 2026-09-22 11:30:33 +02:00
tsconfig.json yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00
vite.config.ts yaderive: days since the last scope change 2026-09-22 11:09:32 +02:00

yadérive

Le compteur de jours sans changement de périmètre, servi sur derive.yagni.fr. « Dérive du périmètre » est le terme français pour le scope creep — et le principe reste fidèle à la marque : aucun backend applicatif.

C'est la version sérieuse d'une private joke : le panneau d'usine « X jours sans accident », appliqué aux specs qui bougent.

Le principe

Une seule image, un seul port, un seul conteneur — comme le blog et yapoker :

docker run -p 8080:8080 -v yaderive-data:/data forgejo.yagni.fr/yagni/yaderive

Dedans, un processus Node qui fait deux choses, et rien de plus :

Ce qu'il sert Rôle Ce qu'il sait des compteurs
GET * le front SvelteKit compilé — c'est lui qui porte toute la logique : jours, séries, record, journal tout, mais dans le navigateur
WS /sync/<compteur> relaie les updates CRDT entre les clients d'un même compteur et les persiste sur disque rien

Le serveur est celui de yapoker, repris tel quel : il ne prononce jamais le mot « poker », et il ne prononce jamais le mot « compteur » non plus. Il relaie des updates Yjs opaques et les écrit dans une base SQLite.

Deux repos, deux images, zéro paquet partagé. Factoriser un hub de 300 lignes pour son deuxième usage serait une blague involontaire sur un projet qui s'appelle yagni ; le jour où il y aura un troisième usage, on en reparlera.

Ce qui est stocké (et ce qui ne l'est pas)

Un compteur, c'est un Y.Doc, dont le nom de document est le slug de l'URL :

meta     Y.Map     createdAt, label
changes  Y.Array   les changements déclarés (JSON figé, jamais réécrit)

Il n'y a pas de lastResetAt, pas de record, pas de nombre de jours. Tout ça se dérive du journal à la lecture (src/lib/counter/streaks.ts).

Ce n'est pas de l'élégance gratuite. Un lastResetAt dans le Y.Map serait le même piège que les votes imbriqués de yapoker : deux personnes qui déclarent un changement en même temps — ou l'une des deux dans le train, qui se resynchronise une heure plus tard — écrivent la même clé, et la fusion CRDT n'en garde qu'une. Un changement disparaîtrait sans laisser de trace, dans l'outil dont c'est précisément le seul travail de ne rien laisser passer.

Un push dans un Y.Array est commutatif : les deux entrées survivent, dans un ordre identique chez tout le monde. Le test qui garde cette propriété est src/lib/yjs/doc.test.ts → « deux personnes qui déclarent en même temps ne s'écrasent pas ». S'il casse, c'est la conception qui est à revoir, pas le test.

Conséquence agréable : aucun état dérivé à maintenir cohérent, rien à réparer, et changer la façon de compter ne demande aucune migration.

Trois pièges, et ce qu'on en a fait

La purge des documents vierges. La persistance supprime les documents où personne n'a jamais rien écrit — voulu, sinon un scan d'URL sèmerait un compteur par adresse essayée. Mais un compteur neuf, à zéro jour et sans changement, est un document vierge. D'où la mise en service explicite, qui écrit createdAt : c'est à la fois l'origine de la première série et ce qui sauve le compteur de la compaction. Ouvrir une URL inconnue ne crée toujours rien.

Les horloges. Le hub ne tamponne pas les dates : il ne connaît pas le contenu des documents, et c'est ce qui fait tout l'intérêt du montage. Les dates viennent donc du navigateur qui déclare. Sur une séance de planning poker, aucune importance ; sur un compteur en jours, un poste mal réglé afficherait -3 ou 4021. Les dates invraisemblables sont écartées du calcul (tolérance de cinq minutes) et signalées dans le journal plutôt que cachées : une déclaration qui disparaît sans explication passe pour un bug.

Le volume. Une vingtaine de changements par an, à une centaine d'octets l'entrée, contre un plafond de 4 Mio par document. Il n'y a aucun pruning à écrire, et il ne faut pas en écrire.

Les natures de changement

Huit cases, définies dans src/lib/counter/kinds.ts :

Nature Ce qu'elle couvre
Ajout en cours de route une demande de plus entre dans le périmètre
Changement de priorité l'ordre des choses à faire est réorganisé
Règle métier modifiée ce qui est vrai dans le domaine change : un calcul, un seuil, un cas à traiter
Fonctionnalité revue le besoin tient, ce qu'on livre pour y répondre change : l'écran, le format, le parcours
Retrait ou report quelque chose sort du périmètre — la seule case qui est souvent une bonne nouvelle
Découverte en cours de route l'équipe a trouvé quelque chose : spec ambiguë, angle mort, infaisable comme prévu
Contrainte externe RGPD, sécurité, client, dépendance — ce que personne autour de la table ne contrôle
Autre filet de sécurité, volontairement en dernier

Le critère de découpage : une case existe si elle mène à une conversation différente en rétro. C'est ce qui sépare règle métier de fonctionnalité revue — une série de la première dit que le domaine n'était pas stabilisé (remède en amont : ateliers, aller voir les vrais experts), une série de la seconde dit que la solution a été validée trop tard (remède : maquettes et démos plus tôt). Deux remèdes, deux cases. Sinon, on fusionne.

L'arbitrage quand deux cases collent (une infaisabilité trouvée par l'équipe change bien la solution) : si le changement vient de l'équipe, c'est découverte, quoi qu'il bouge.

À surveiller : si autre dépasse une déclaration sur cinq, ce n'est pas que les changements sont inclassables, c'est qu'il manque une case — et le journal perd sa valeur tant qu'on ne l'a pas ajoutée.

La taxonomie a déjà changé deux fois (interlocuteur, une origine déguisée en nature ; exigence, du vocabulaire de cahier des charges que personne n'employait). Les entrées anciennes restent lisibles : le journal affiche la valeur brute d'une nature retirée plutôt qu'un badge vide, et kinds.test.ts le verrouille.

Ce que ce n'est pas

Un outil de surveillance, ni un dossier à charge. Les natures de changement nomment ce qui a bougé, jamais qui l'a demandé — c'est aussi pour ça que l'app ne s'appelle pas « yagirouette ». Un panneau affiché dans un couloir se lit très vite comme un doigt d'honneur permanent adressé à quelqu'un.

Et toutes les cases ne décrivent pas des choses qu'on subit : découverte en cours de route couvre le périmètre qui bouge parce que l'équipe a trouvé quelque chose (spec ambiguë, angle mort, infaisable comme prévu). Sans elle, le panneau ne tient pas dix minutes en rétro. Le compteur mesure la stabilité du périmètre, pas les torts de qui que ce soit.

Il n'y a ni compte, ni mot de passe, ni permissions : qui a l'adresse peut lire, déclarer et corriger. Outil interne entre collègues de confiance, même arbitrage que les cartes visibles dans les devtools de yapoker.

Mode affichage

/panneau/<compteur> rend le panneau seul, en grand, sans bouton ni journal : pour un écran de bureau, ou pour montrer le compteur à quelqu'un d'extérieur à l'équipe. Le bouton « copier le lien d'affichage » est sur la page du compteur.

Ce n'est pas une permission, et l'UI le dit : qui a cette adresse peut aussi ouvrir /compteur/<compteur> et déclarer. Ça évite les clics malheureux, pas les gens décidés. Une vraie lecture seule demanderait un jeton d'écriture vérifié par le hub — faisable sans lui apprendre ce qu'il relaie, mais ça lui donne un état à gérer et des jetons à distribuer, pour un compteur de vannes.

Ce qui rend l'arbitrage confortable : une remise à zéro accidentelle est entièrement réversible. Comme rien n'est stocké à part le journal, retirer l'entrée fautive recalcule les jours, la série et le record à l'identique. Il n'y a aucun dégât permanent à prévenir.

Pourquoi un groupe de routes

src/routes/+layout.svelte              feuille de style + favicon, rien d'autre
src/routes/(app)/+layout.svelte        thème, largeur de lecture, pied de page
src/routes/(app)/+page.svelte          accueil          -> /
src/routes/(app)/compteur/[id]/        le compteur      -> /compteur/<id>
src/routes/panneau/[id]/               l'affichage      -> /panneau/<id>

Le layout racine s'applique à TOUTES les routes, sans exception : +page@.svelte remonte jusqu'à lui et ne s'en détache pas (vérifié dans le manifest de build — c'était une erreur de conception au premier jet). Pour qu'une page occupe tout l'écran, il faut donc que l'habillage vive plus bas, dans un groupe. Les parenthèses ne changent aucune URL.

Démarrage local

Prérequis : Node 24 et just. Le flake les fournit (nix develop, ou automatiquement avec direnv après direnv allow).

just install     # dépendances de l'app et du serveur
just server      # terminal 1 : serveur sur :8080
just dev         # terminal 2 : Vite (live-reload) sur :5173

Puis http://localhost:5173. Vite proxifie /sync vers le serveur, donc le client parle toujours à sa propre origine — exactement la situation de prod. Aucune URL à configurer.

Commande Ce que ça fait
just serve build + serveur, sans Docker -> http://localhost:8080
just image construit l'image unique (yaderive:dev)
just run [port] construit puis lance l'image comme en prod (just run 8088)
just clean-data supprime le volume Docker de test
just test dérivations du compteur (vitest) + persistance (node --test)
just check typecheck Svelte + TypeScript
just counters les compteurs présents dans la base locale
just drop-counter <nom> supprime définitivement un compteur et son journal

Déploiement

k8s/ contient des manifests kustomize prêts pour un dépôt GitOps (Argo CD), et .forgejo/workflows/deploy.yml construit l'image avec kaniko puis bumpe le tag. Les deux sont repris de yapoker ; les seules différences tiennent en une ligne :

  • Recreate, un seul replica, jamais de scale-out. Un seul processus doit écrire la base SQLite, et deux pods ne partageraient pas leurs documents en mémoire.
  • Le PVC porte Prune=false. Contrairement à une room de planning poker, ces données sont faites pour durer des années : c'est le volume à sauvegarder, et le perdre remet toutes les équipes à zéro jour.

Choix assumés

  • Pas d'i18n. yapoker parle français et anglais ; ici tout est en français. Une vanne sur la dérive du périmètre ne se traduit pas, et le jour où il faudra, src/lib/i18n/ de yapoker se recopie en une heure.
  • Pas de générateur de nom rigolo. Une room est jetable et doit se dicter au téléphone ; un compteur est un marque-page qu'on retape pendant des mois.
  • Pas de graphique. Le journal dit déjà « a cassé une série de 12 jours » à chaque ligne. Le jour où six mois d'historique tiendront dans une sparkline, ce sera peut-être utile — pas avant.