Skip to content

🚒 Release & rollout

A platform change is built once in the platform repo, then rolled out to every own app from shared-core, and every app is checked on a local stack before it ships.


πŸ” Changing the platform

Every change β€” a feature, a bug fix, a new rule or check β€” is made once, in the platform repository, then rolled out to the apps from shared-core. Two Claude sessions, two steps.

PLATFORM SESSION platform repo ROLLOUT SESSION shared-core, background build contract Β· code Β· RefApp Β· a check PLATFORM_VERSION Β· release note /release <version> survey every app plans in .fleet/ ship RefApp refapp/deploy.ps1 start the rollout start-rollout.ps1 <version> triage the INBOX fix now, or ride the next release you decide apply each app: adapt Β· gate commit you: sign-off gates align fleet-lockfiles -Update check review Β· fleet-check on local stacks you: off plan, stuck ship each deploy.ps1, until production runs it you: the push record rollouts/<version>.md a message back
Two sessions and four moments that are yours: the sign-offs, what goes off plan, the push, the INBOX.

1. Build it β€” in the platform repository

Open a session in platform and ask for the feature or the fix. The session first reads INBOX.md (problems a rollout found), then delivers in one pass: the code and its contract (contracts/*.md), RefApp using it, a check proving it (HTTP or browser scenario), the gates β€” including shared-core/scripts/fleet-check.ps1 -Apps refapp β€” a bumped PLATFORM_VERSION and the release note releases/<version>.md. Then /release <version> finishes it: a read-only survey of every app against the draft note (whatever it missed is fixed there, before the version exists), the commits, RefApp's deploy.ps1 (https://platform.domain.fr/api/health names the new version), and the rollout started as a background session of shared-core. RefApp's suites run once, in that fleet-check; the pre-flight on the apps furthest from it checks their source, HTTP and NGINX only. Hold the rollout back and /release offers the full suite (fleet-check -Own) instead.

Kind of change Version The release note says
Bug fix patch β€” 0.3.1 Automatic: apps get it by rebuilding
New feature minor β€” 0.4.0 Opt-in: which apps it applies to, how to adopt it
New rule or stricter check apps must meet minor Breaking changes: the edits each app needs
Test-only fix (a check made accurate) none no release β€” fleet-check uses it at once
Correction of a published note none corrected in place, listed in the next note β€” no rollout

A release with only Automatic items takes the rollout's fast path: a scripted PLATFORM_ADOPTED bump per app, no survey or agents. An INBOX fix production is not suffering from waits for the next feature release.

2. What the fleet receives

File Repository Written by Purpose
releases/<version>.md platform platform session the rollout's brief: automatic, breaking, opt-in, server actions
INBOX.md platform a rollout a platform problem met in an app β€” fixed by the platform session first
rollouts/<version>.md shared-core the rollout the approved plan and the outcome, per app
PLATFORM_ADOPTED each app the rollout the platform version the app runs

3. Roll it out β€” in shared-core

/release starts it for you (shared-core/scripts/start-rollout.ps1 <version> does the same by hand; /fleet-rollout <version> in an open shared-core session too). The rollout applies every release note between each app's PLATFORM_ADOPTED and that version, skipped versions included, reusing the survey /release made. While its reviews run, each app's images are built ahead (fleet-check -Prebuild), so its local checks wait on no build. It asks you only four kinds of things β€” each shows as Needs input in claude agents (Space opens it, the option's number answers):

  1. Sign-off gates β€” an app whose plan touches a new dependency, nginx, a quadlet, an env key, a Dockerfile or the docs structure, with its server actions.
  2. Off-plan or stuck β€” a review finding outside the plan, or an app still red after two fixing attempts.
  3. The push β€” once, when every approved app was adapted, gated, committed, reviewed and checked locally, with the list of what the rollout decided without you. The local check it ships on is fleet-check -Smoke: every column in full but the browser suite's, which runs the scenarios marked 🚒 below, about a minute per app. Each app ships through its deploy.ps1, which waits until production runs the shipped commit.
  4. The full suite β€” once shipped, whether to run fleet-check -Own now, every scenario of every own app; its failures land in the rollout's record and in INBOX.md.

When it ends, it messages the platform session with its outcome and the INBOX entries it filed; that session proposes what to do with each, and you decide. fleet-check.ps1 -All re-checks the whole fleet at any time.

Rollback: git revert the platform commit, push (RefApp redeploys), and roll out the fixed version.


βœ… Fleet conformance

LOCAL STACK run-local.ps1 -Detach Β· Docker Desktop nginx Debian's nginx Β· the house files the repo's vhost, unchanged https://127.0.0.1:<nginx.port> backend the runtime image + its sidecars 127.0.0.1:<app port> postgres dev admin: password + a second user 127.0.0.1:<db port> FLEET-CHECK one row per app Β· -Parallel auto source a static read of the backend http conformance Rust Β· contracts browser Playwright offline scenarios app e2e the app's own e2e/ suite nginx nginx-check as deployed every suite but source goes through nginx
One local stack per app, the server's own nginx in front, five columns in one table.

Two suites check that an app still honors the platform contracts, on a local stack only: production is never tested. The HTTP suite (conformance/, cargo) reads the contracts over HTTP: health, error envelopes, cache and security headers, the worker and manifest, the sync endpoints, the sign-in and device round trips. The browser suite (conformance/browser, Playwright on Chromium) sees what HTTP cannot: a reload with no network, a network that accepts requests but never replies, server errors that must not sign the device out, an edit made offline reaching another device live. Each app signs in through a small adapter in conformance/browser/apps.ts, and each app's own e2e/ suite runs right after it (RefApp's holds its notes, covers and images scenarios: images dropped offline, uploaded later and kept by tier).

Both run per app from the shared-core repository, in one table:

# from the shared-core checkout
./scripts/fleet-check.ps1 -Apps refapp          # a local stack: start, check, stop
./scripts/fleet-check.ps1                       # the apps whose repos changed
./scripts/fleet-check.ps1 -All                  # every own app and every cloned app's NGINX
./scripts/fleet-check.ps1 -Own                  # every own app, RefApp included
./scripts/fleet-check.ps1 -Own -Smoke           # the browser suite's 🚒 scenarios only
./scripts/fleet-check.ps1 -Own -Prebuild        # build each stack's images, check nothing

fleet.json lists the fleet (paths, URLs, local sign-in, the sync collection the credentialed round-trip may use). Each app starts run-local.ps1 -Detach -ResetDevAdmin on its own ports from fleet.json (its admin signs in with the dev password password), is checked, then -Stop. Every run-local.ps1 (the own apps' since platform 0.19.0) puts the server's own NGINX in front of the stack (Debian's nginx, the house nginx.conf, the app's vhost unchanged, on https://127.0.0.1:<port>): every suite then goes through it, and a last nginx column checks NGINX itself β€” TLS and HTTP/2, each security header once, body caps, rate limits, SSE, WebSockets, the maintenance page. One exception since platform 0.30.0: what the suites send to the sign-in routes goes to the app's own port, where no rate limit waits, so no check sleeps on NGINX's sign-in limiter; the nginx column still proves that limit. Every own run-local.ps1 (since platform 0.27.0) also starts Mailpit on the app's own inbox port from fleet.json: every mail of the run lands there and none goes out, and the account tests open their mailed links from it. The cloned apps get the nginx column only. With no switch, it checks the apps whose repos hold uncommitted or unpushed work (a platform change to its crates, packages or suites: every own app); -Plan says which, and why, without running anything. As many apps run at once as memory holds, the longest first, their stacks starting one at a time. Each suite runs under a watchdog, and a scenario that passes only on its retry is reported flaky. Under the table, each failure gets a short section: the test, its error, where it failed, its log and the command that re-runs the app; the same report lands as Markdown in shared-core/.fleet/logs/.


πŸ§ͺ E2E tests

Both Playwright suites drive a real Chromium, headless, on the local stack behind its NGINX: they click and type, cut or swallow the network, restart the PWA, and check what the screen shows. Since platform 0.31.0 the browser suite runs on several Playwright workers per app (E2E_WORKERS), each on accounts of its own β€” an admin and a second account the run's admin invites and deletes again β€” so no two workers share rows; the scenarios that act on the whole server (the admin's list of every account, the last admin, the device limit) run after them, alone, on the dev admin β€” and so, on an app whose rows every account sees (Kouizine, LightFlow), do the two that read the whole store. Each app's own suite runs on one worker, its tests one at a time; -Parallel runs several apps at once. Each app's own suite reuses the browser suite's session. What a test sends to the sign-in routes goes to the app's own port (platform 0.30.0), so nothing waits on NGINX's sign-in limiter; any other request to a rate-limited route waits for NGINX's allowance.

🌐 Every app: the browser suite

platform/conformance/browser, driven per app by its adapter in apps.ts. The ids are those of shared-core's nonregression-plan.md, which keeps the matrix. The cells are the 0.27.0 rollout's local run, 2026-10-08, and A14–A16's a fleet-check -Own run on 2026-10-10, each first seen failing against a deliberate break; every E test (the account pages, platform 0.27.0) was first seen failing against a deliberate break. Since platform 0.41.0 the account edits run on every app (E5, E7, E8, E10, E11, E13, E14, E21–E25): no app's profile is the platform's, and five draw their own accounts page. The rest of the account pages runs in full on RefApp only (platform 0.30.0); every app runs their smoke (E1, E2, E19), and an app whose sign-in screens are its own also checks what those show (E16, E17). E20, the phone width (platform 0.34.0), runs on every app: each styles those pages itself. So does E26 (platform 0.42.0), the two-factor and close-account cards side by side: each app lays them out.

βœ… covered Β· ◐ partial (proves less than its title, and says so) Β· β†’ covered by the app's own suite Β· β—‡ checked once, on RefApp Β· β€” not applicable Β· ❌ missing Β· ⏳ not run on this app yet (added by a release; the next full run fills it) Β· 🚒 the ship set (platform 0.31.0): what a rollout ships on, about a minute per app (E2E_RUN=ship, fleet-check -Smoke); every scenario runs after the ship, when you ask
CB CardBox Β· CN ClaudeNames Β· KZ Kouizine Β· LF LightFlow Β· MS MediaScope Β· PP PooPing Β· RA RefApp Β· TW TripWise

Scenario CB CN KZ LF MS PP RA TW
A1 🚒 Offline cold start: shell, rows and files within 3 s, sync signal "Offline" βœ… ◐ βœ… βœ… ◐ βœ… βœ… βœ…
A2 A row created offline syncs by itself, live on an open device βœ… β€” βœ… βœ… β†’ βœ… βœ… βœ…
A3 🚒 An offline edit and delete sync the same way βœ… β€” βœ… βœ… β†’ ◐ βœ… βœ…
A4 A file added online shows at once, live on an open device βœ… β€” βœ… β€” β€” β€” βœ… βœ…
A5 A file added offline: painted, uploaded later, on every device βœ… β€” βœ… β€” β€” β€” βœ… βœ…
A6 The outbox survives a restart while offline, then syncs βœ… β€” βœ… βœ… β†’ βœ… βœ… βœ…
A6β€² After an offline restart, the network's return syncs with no action βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
A7 🚒 Sign-out and a 401 leave nothing on the device βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
A7β€² A sign-out made offline holds when the app next starts online βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
A8 A deep link opened offline renders from the device βœ… β€” βœ… β€” βœ… β€” β€” βœ…
A9 Every main route renders, online and offline βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
A10 Online-only screens say they need a connection (deferred) ❌ ❌ ❌ ❌ ❌ β€” β€” ❌
A11 No page error, CSP refusal or undeclared storage key, anywhere βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
A12 Another account on the device never sees the first one's rows βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
A13 A row on screen survives an immediate reload ◐ β€” ◐ βœ… β†’ βœ… βœ… βœ…
A14 Nothing new: a restart online never reloads the page βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
A15 A new build reloads the open page once, shell and rows intact βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
A16 Only data changed: another device's row shows, with no reload βœ… β€” βœ… βœ… β€” βœ… βœ… βœ…
A17 A backend stopped behind the proxy: "Offline", a change waits, then syncs by itself βœ… β€” βœ… βœ… β€” βœ… βœ… βœ…
S1 A share reaches the recipient live and survives an offline restart βœ… β€” β€” β€” β€” β€” βœ… βœ…
S2 The lowest role shows no edit control βœ… β€” β€” β€” β€” β€” βœ… βœ…
S3 A role change grants editing live; the edit reaches the owner β€” β€” β€” β€” β€” β€” βœ… βœ…
S4 A revoke takes the item off the recipient's devices live βœ… β€” β€” β€” β€” β€” βœ… βœ…
S5 A revoke made offline lands once back online βœ… β€” β€” β€” β€” β€” βœ… ◐
S6 The recipient leaves: the item goes, the owner's list drops them βœ… β€” β€” β€” β€” β€” βœ… βœ…
S7 The owner's delete takes the item off the recipient's devices live βœ… β€” β€” β€” β€” β€” βœ… βœ…
E1 🚒 The right password signs in, and the page shows the account βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
E2 🚒 A wrong password is refused with its sentence, and signs nobody in βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
E3 Every signed-in route, opened signed out, lands on sign-in β—‡ β—‡ β—‡ β—‡ β—‡ β—‡ βœ… β—‡
E4 A non-admin sees no admin entry, and an admin route sends it away β—‡ β€” β—‡ β—‡ β—‡ β—‡ βœ… β—‡
E5 A password change refuses a wrong current password and a mismatch βœ… β€” βœ… βœ… βœ… βœ… βœ… βœ…
E6 Two-factor: set up from the key shown, asked for at sign-in, turned off β—‡ β—‡ β—‡ β—‡ β—‡ β—‡ βœ… β—‡
E7 A new address moves only once its mailed link is opened βœ… β€” βœ… βœ… βœ… βœ… βœ… βœ…
E8 Forgot password: one answer for any address, a mailed link that works once βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
E9 The admin's invite mails a link that activates the account β—‡ β—‡ β—‡ β—‡ β—‡ β—‡ βœ… β—‡
E10 A dead activation link says so βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
E11 The admin edits an invited account's names βœ… β€” βœ… βœ… βœ… βœ… βœ… βœ…
E12 The admin grants and revokes the admin role, never their own β—‡ β€” β—‡ β—‡ β—‡ β—‡ βœ… β—‡
E13 The admin re-sends a link, and the mail arrives βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
E14 The admin deletes an account, which can no longer sign in βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
E15 Closing one's own account: a wrong password and the last admin refused β—‡ β—‡ β—‡ β—‡ β—‡ β—‡ βœ… β—‡
E16 One device too many is refused; the admin's forget lets it in β€” β€” β€” β€” βœ… β€” βœ… β€”
E17 Five wrong passwords lock the account, and the sign-in says so β—‡ βœ… βœ… βœ… βœ… β—‡ βœ… β—‡
E18 An unknown path shows the 404 page and its way back β—‡ β€” β—‡ β€” β€” β—‡ βœ… β—‡
E19 🚒 Signing out through the app's own menu ends the session βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
E20 🚒 The account pages and their dialogs fit a 365 px phone βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…
E21 The admin moves an active account's address: written at once, both addresses told; never their own βœ… β€” βœ… βœ… βœ… βœ… βœ… βœ…
E22 Own names on the profile show there and in the app's menu, after a reload too βœ… β€” βœ… βœ… βœ… βœ… βœ… βœ…
E23 The admin mails an active account a reset link that works βœ… β€” βœ… βœ… βœ… βœ… βœ… βœ…
E24 A stale profile page saves only what its user changed βœ… β€” βœ… βœ… βœ… βœ… βœ… βœ…
E25 A corrected pending address gets the invite; the old link is dead βœ… β€” βœ… βœ… βœ… βœ… βœ… βœ…
E26 Two-factor and closing the account share a row, their buttons level at its end, no label broken at 581 px; stacked on a phone βœ… βœ… βœ… βœ… βœ… βœ… βœ… βœ…

A17 (platform 0.32.0): the proxy's maintenance page (502, HTML) on every /api route no longer counts as proof that the backend is there, so a stopped backend turns the online dot red within about 6 s. First seen failing on RefApp's build from before the fix ("1 pending", never "Offline"). TripWise's own fetch layer weighs the proxy's answers the same way; a unit test of its own holds that, since A17 cannot see it.

◐: ClaudeNames and MediaScope have no labelled rows for A1 to look for on screen; a PooPing entry cannot be edited (A3 deletes only); CardBox and Kouizine show a row only once its save lands, so A13's reload proves nothing; TripWise's shares are online-only (S5). β†’: MediaScope's marks carry no label to look for, so its own suite covers them (below). The suite also keeps scenarios written after real bugs (identity, sign-in, email confirmation, file replace, blob versions, the maintenance page, the login picture, a row made while another device's change stream is still connecting, an answer from before the network went away that must not keep the device online) and the profile picture's device half (platform 0.28.0: kept in the eager cache under its version, the old one evicted, served offline by the worker, wiped by a sign-out β€” adopters only): they run on every app but are not matrix rows. The login picture's are cache first since platform 0.36.0: a first visit waits for the picture, 3 s at most, a returning device shows its copy at once, and a new picture shows on the next visit. RefApp shows its picture or its scene per device since platform 0.37.0, and the picture's scenarios set its switch to the picture; they count only the picture's own requests, never a login theme's. The sync state is read from the store's <html> signal (data-sync-state, data-sync-label), the same on every app, since the sync dot sits in a closed panel or nowhere.

🚒: A7's sign-out half (its 401 half runs in the full suite); A3's edit and its delete, so an app that cannot edit a row (PooPing) still ships on one.

E runs on throwaway accounts the admin invites and deletes again, never on the dev admin, and reads the mail from the stack's Mailpit. The account edits run on every app since platform 0.41.0; the 0.41.0 rollout's full run passed them on every app (2026-10-26). β—‡: since platform 0.30.0 the rest of the account pages runs in full on RefApp only; the app runs their smoke (E1, E2, E19) and, when its sign-in screens are its own (ClaudeNames, Kouizine, LightFlow, MediaScope), the lockout's sentence (E17) and its device-limit panel (E16). E2E_ACCOUNTS_CATALOG=full runs the whole catalog on any app, by hand. Every app lets an account close itself since 0.30.0 (E15). β€”: ClaudeNames' accounts are all admins, with no profile page and no account to edit, and it re-sends a pending account's invite only (E4, E5, E7, E11, E12, E21–E25); only MediaScope and RefApp limit an account's devices (E16); LightFlow, MediaScope and ClaudeNames send an unknown path home, with no 404 page (E18).

E20 (platform 0.34.0, phone.spec.ts): every app's account pages at 365Γ—780 on a touch screen. It opens the sign-in page, the account's page, its menu, the profile picture's crop dialog (adopters), the two-factor setup, the close-account confirmation, the accounts page and the 404 page. On each it measures, with @platform/e2e's expectFitsPhone: - no sideways scroll; - no control past the edge, cut by a box that hides its overflow, or reachable only through a box's sideways scroll; - no text past the edge, nor cut by such a box; - a 24 px hit area for every control; - nothing shown only on hover; - every dialog inside the screen.

It runs after the workers, alone, as the dev admin. That account has no name on a local stack (run-local.ps1 -ResetDevAdmin blanks it, LightFlow's and MediaScope's seeded "Admin" included), so its email is its page's heading.

It was first seen failing on RefApp's build from before the release: the profile email widened the page to 422 px, the crop dialog sat past the edge, the sync dot was a 12 px target, and the accounts table hid its actions behind a sideways scroll. It runs on every app, since each app styles those pages itself. An app's own pages are its own suite's, and shared-core's fleet-screens.ps1 audits them.

E26 (platform 0.42.0, account-cards.spec.ts): the account page's two-factor card and close-account card share one row, two equal columns above 580 px, stacked on a phone. A throwaway account opens the page at 1280Γ—720 with two-factor off, then with its setup open, then on. In each state the two cards share a top edge and a height, their buttons a bottom edge, and each button stands as far from its card's right edge as the other. Each state is measured again at 581 px, the narrowest two columns, where no button may break its label across lines (platform 0.43.0): the row wraps and drops a whole button instead. At 365 px the cards stack. The adapter names each app's two cards (apps.ts accounts.cards). It came from TripWise, where "Delete my account" sat higher than "Disable two-factor", and at the other end of its card.

The sign-in scene and the login theme (platform 0.37.0) have specs of their own, not matrix rows either. Each runs on the apps whose sign-in page or route says they have one ([data-login-scene], /api/brand/login-theme): RefApp, CardBox, LightFlow, MediaScope, PooPing and TripWise; never ClaudeNames or Kouizine, which have neither. Every scenario was first seen failing against a deliberate break on RefApp. - login-scene.spec.ts: the scene's steady state moves only transform and opacity, on HTML elements or whole <svg>s (waivers named per app in apps.ts); in 3 s it fires no frame callback but the runtime's own frame loop, which the runtime counts (TripWise's globe declared, 30 fps at most; since platform 0.37.1, so a scene's own loop shows however slowly headless Chromium's software WebGL runs it); reduced motion stills it and moves it again, live, and a device that asks for it gets it still from the start; a hidden tab pauses it and its return plays it; a second visit shows it offline; the sign-in form and a dead link's page are painted over it; once the signed-out pages are left, none of its animations runs on (platform 0.37.1). - login-theme.spec.ts: the sign-in page's theme at noon and at 20:00 on the device's clock, its color-scheme and theme-color with it, from the first paint; system follows the colour scheme live; the night's start crosses on an open page; a signed-in page carries none, even when its first paint guessed signed out; signing out lands in it.

The glass sign-in card's look is no suite's (platform 0.39.0, which retired 0.38.0's F1–F3 and every app's legibility test): whoever sets an app's values checks them by eye.

🧩 Each app: its own suite

<app>/e2e/tests/*.spec.ts (RefApp's in platform/refapp/e2e), run right after the browser suite. Each test creates only content labelled e2e … and removes it in a finally.

πŸ’³ CardBox

Spec What it tests
barcode An EAN-13 card renders a non-empty barcode
barcode A bad check digit is refused by the editor
barcode The server refuses a bad check digit pushed past the editor
scan The photo scan reads an EAN-13 online
scan Offline, the photo scan is disabled and says it needs a connection
share A read-only share; the colour the recipient picks reaches the owner live
dots The online dot sits bottom-left on the top bar's avatar: green, red once the backend is cut, green again
dots The sync dot in the menu's header: green when synced, red offline with a card waiting, orange while the round runs
dots Hovering the sync dot opens nothing; a click opens its popover, Sync now runs a round, and a second click or a click elsewhere in the panel closes it; Enter opens it and Escape closes it
dots On a touch screen, a tap on the sync dot opens the popover and a tap elsewhere in the panel closes it
dots The panel: the avatar with its sync dot, the name and the email, then Profile, Users for an admin, and Sign out; the second dev user sees no Users, and its Sign out goes to /login
email At 365 px an account's e-mail breaks only after its @ or before a dot, never inside a word: the accounts page, the account menu's header, the profile's header and the share picker
scene Signed out, the sign-in card sits over the animated scene: its 14 cards drawn behind the card, and the scene's loops run (every card floats, the hands breathe, the wall drifts)
scene The deal-in lands exactly on the picture, then a card moment plays and the card comes back to its place
scene Under reduced motion the scene is the still picture: every card on the picture (that no loop runs is the platform's login-scene.spec.ts)
scene The signed-out pages take the login theme by the device's clock: dark with the night scene at 20:00 on a light device, light with the day's wall at noon on a dark one
scene Signed in, the account's own theme takes over: a new account's System theme follows the device, live, and no login theme is left (a scratch account)

πŸ•΅οΈ ClaudeNames

Spec What it tests
upload An admin upload shows its per-file progress, arrives by sync and reaches another admin device live
room A room joined over the WebSocket deals its 25-card board to every player
pictures A character's preview is stored once under its SHA-256, cached for good, and painted offline
dots No status dot, sync dot or pill on /admin, on / or in the admin menu, online, offline and back online, while the <html> sync signal is up
dots A signed-out device has neither the <html> sync signal nor a dot
email At 365 px an admin's e-mail address breaks only after its @ or before a dot, never inside a word: in the Comptes list, the admin menu and the activation page

🍴 Kouizine

Spec What it tests
recipe A recipe reaches its public page for a signed-out visitor, whose panier lists its ingredients
recipe The panier adds two recipes up to the chosen parts and follows a change made offline
recipe Β« Nouvelle recette Β» from an editor starts empty and saves a second recipe beside the first
basket The basket and its ticks reach the account's other device and survive an offline restart
dots The online dot sits bottom-left on the menubar's avatar: green, red once the backend is cut, green again
dots The sync dot in the user menu's header: green when synced, red offline with a change waiting, orange while the round runs
dots Hovering the sync dot opens nothing; a click opens the popover over the menu's width, Sync now runs a round, and a second click or a click outside closes it; Enter opens it and Escape closes it; the header shows no role, the items keep their order
dots On a touch screen, a tap on the sync dot opens the popover and a tap outside closes it
dots A signed-out visitor has no circle and no dot
avatar A JPEG XL picture on RΓ©glages: cropped to a 512 px JPEG, shown on the menubar and in the user menu, kept in the eager cache, painted offline with its controls locked, replaced and removed
phone At 365 px an account's email breaks at its @ or a dot, never inside a word: the accounts page, the user menu's header, the activation page

πŸͺΆ LightFlow

Spec What it tests
run-flow A pool, a task and an echo step run at once, and the run and its log reach another device by sync
viewer A viewer's device never receives variables, while the pools reach it live; its account menu has no Users
sign-out Signing out offline wipes the device
sign-out Signing out while the server is down wipes the device
dots The online dot sits bottom-left on the top bar's avatar: green, red once the backend is cut, green again
dots The account menu's sync dot: green when synced, red offline with a pool waiting, orange while the round runs
dots Hovering the account menu's sync dot opens nothing; a click opens its popover, Sync now runs a round, and a second click or a click elsewhere in the panel closes it; Enter opens it and Escape closes it
dots On a touch screen, a tap on the sync dot opens the popover and a tap elsewhere in the panel closes it
dots The shell has no sync pill, no sidebar user block and no Users in the sidebar; the avatar opens the account menu: the sync dot, the name and the email, then Users, Profile and Sign out; the profile has no Sign out and no role or status badge
email At 365 px an e-mail address breaks at its @ or a dot, never inside a word: in the Users page, the account menu and the profile (platform 0.35.0)
login-scene The sign-in page draws the animated lighthouse behind its card, every animation a Web Animation of transform or opacity; the retired login picture is never asked for (its own route, /api/brand/login-picture)
login-scene The login theme is the night scene: dark at 20:00 on the device's clock, light at noon, whatever the device's own theme (the night copies, the moon and the stars)
login-scene Reduce motion's still frame is the former login picture: no cloud and no window light
login-scene The activation and email-confirmation pages draw the scene too, whatever their link says

🎬 MediaScope

Spec What it tests
wall The poster walls paint from the device with the network cut
viewed A viewed mark set offline syncs by itself and reaches an open device live
playback A playback position saved on one device is offered on another (in Edge)
downloads A paused download survives a reload: the row comes back paused and resumes to the whole file, which starts with the share's bytes
downloads A download cut by a closed browser, without a pause, resumes after its last committed 128 MiB block, not from the start, and the file ends whole
browse A film's panel shows its size, resolution, track languages and synopsis, and closes on Escape or a click outside
browse Type-to-find narrows the wall, says when nothing matches and clears on Escape; the arrow keys hop between the walls
browse A film is found by its French title, case and accents ignored
series The top bar's eye hides the viewed titles, and the choice survives a reload
series A series' page lists its seasons and episodes; its episode, season and card eyes mark what they cover
overrides The TMDB match title override is saved and cleared
overrides A season's slots and one episode are approved, then cleared
overrides The ordering panel compares TMDB's orderings with the disk, and says when no token is configured
sources A data source is refused without a host, added, edited with its type locked, switched off and deleted
sources The connection test reaches the share
settings The scan, weekly email and System settings are saved, and values out of range refused
settings The TMDB card refuses an empty save, clears the token and saves one
settings A manual scan from the top bar runs and reports in the Last scan card
appearance A theme, a top-bar font and the A–Z rail are saved to the account and applied
wishlist A wish asked on the profile is listed open and survives a reload; a scanned film's name says the library holds it; the admin refuses it on the Users page, then reopens it, and the profile follows; its bin removes it
wishlist A wish named after a scanned film's French title is marked found by the next scan, links to the film's player, and its owner gets one "in the library" mail
nonadmin A plain account has no admin control, and the admin routes send it to the films wall
statistics The Statistics page charts the sources, the visitors, the downloads and the live traffic
reset Reset library empties both walls, which say to add a data source, and a scan brings the library back
dots The top bar shows no online dot and no pill, even offline
dots The sync dot sits bottom-left on the user panel's avatar: green synced, orange while a round runs, red offline, green again
dots Hovering the sync dot opens nothing; a click opens its popover inside the panel, Sync now runs a round, and a second click or a click in the panel closes it; Enter opens it, Escape closes it with the panel
dots On a touch screen, a tap on the sync dot opens its popover, a tap inside keeps it, a tap elsewhere in the panel closes it
dots The panel's head: the avatar with its sync dot, the name and the email, no role, outside the menu, whose items are unchanged
email At 365 px an account's e-mail address breaks at its @ or a dot, never inside a word, in the Users page, the account menu and the profile
login-scene The sign-in page builds its animated poster wall at once under the platform's backdrop host (LoginBackdrop), no poster twice among the columns in view before and after a drift step (the columns ordered by their translateX), every loop a Web Animation; the retired picture's own route (/api/brand/login-picture) is never asked for and answers 404, the login theme's /api/brand/login-theme not counted
login-scene Reduce motion shows the settled wall, still, the app's own still frame: every card in, every circuit drawn, no poster twice, nothing running (the platform's login-scene.spec.ts holds live reduced motion and the hidden tab)

The library flows run on a real share: each local start adds two data sources on the folders the local env file names and scans the 5 smallest films and 3 smallest series (e2e/seed.mjs). The subtitles pass stays off.

πŸ’© PooPing

Spec What it tests
ping One tap logs an entry: a History row, the timer resets, the statistics and the two-week view count it; a second tap within five minutes replaces it
dots The online dot sits bottom-left on the top bar's avatar: green, red once the network is cut, green again
dots The sync dot in the account menu: green when synced, red offline, orange while the reconnection's round runs, then green again
dots Hovering the sync dot opens nothing; a click opens the popover, Sync now runs a round, and a second click or a click outside closes it; Enter opens it and Escape closes it
dots On a touch screen, a tap on the sync dot opens the popover, a tap inside keeps it, a tap outside closes it
dots The menu: the avatar with its sync dot, the name and the email, no role, then Profile, Users for an admin and Sign out; the second dev user sees no Users
email At 365 px an account's e-mail address breaks at its @ or a dot, never inside a word: in the account menu, the profile's header and the accounts page
looks Each theme in turn (Rainbow Land, Poop Quest, Poop Pal, switched on the account and put back) draws the two-week card's clock view at full size: the dial, the radar, the day ring
scene Signed out at noon, the sign-in card sits over the moving day scene: the device's clock pinned at local noon, the scene behind LoginBackdrop ready, its loops run, the day sky paints behind the card
scene The poops watch the email being typed (their pupils follow the caret) and cover their eyes on the password while the peeker ducks; leaving the fields, they are back to the picture
scene Under reduced motion the poses still swap: the password still covers their eyes (the still frame itself is the platform's login-scene.spec.ts)

πŸ› οΈ RefApp

Spec What it tests
notes A note written on one device is edited on another, and the edit comes back live
notes The home page's lines and its Open notes button stand apart
notes At 365 px a note's bin sits in its card's corner, and the body takes the card's width
notes-offline Notes open from the device while the network swallows every request
notes-offline A note written offline reaches another open device without a reload
notes-offline A device that never synced says so instead of "No notes yet."
notes-offline A sync round that dies while the network looks good runs again on its own
images Images added offline show at once, upload once back online and reach every device
covers Replacing a cover: no flash of the old one, no download of the new one, no pile-up
covers Replacing a cover not uploaded yet shows the new one at once
covers Code outside React reads a cover that is still only on this device
covers A 401 on a cover download signs the device out
avatar A JPEG XL picture: cropped to a 512 px JPEG, kept in the eager cache, painted offline, replaced and removed
dots The online dot sits bottom-left on the avatar: green, red once the backend is cut, green again
dots The sync dot: green when synced, red offline with a change waiting, orange while the round runs
dots Hovering the sync dot opens nothing; a click opens its popover, Sync now runs a round, and a second click or a click outside closes it
dots On a touch screen, a tap opens the sync popover and a tap outside closes it
dots The panel: the avatar with its sync dot, the name and the email, then Users for an admin, Profile and Sign out
profile The Account form's Save changes stands clear of its last field
profile At 365 px an email address breaks at its @ or a dot, never inside a word: in the accounts page, the account menu and the profile (platform 0.35.0)
shares The directory lists the second account, and an editor's edit reaches the owner live
shares A viewer reads the note, its cover and its images offline, with no control to change them
backdrop The sign-in page's Picture Β· Animation switch shows the scene by default, then the picture, and the device keeps its choice across a reload (platform 0.37.0)

🧭 TripWise

Spec What it tests
budget A trip gets items, the planner shows them on their day and the budget totals them
attachment-offline A PDF attached offline stays on the device, uploads by itself and opens offline after a restart
attachment-quota With the basic allowance at 1 MB, a basic account's new trip reads "0 B of 1 MB" in its Storage card; a 1.5 MB PDF is refused on the device, a sync create declaring 1.5 MB is rejected by the server, and a 1 KB create's 1.5 MB upload gets a 413 with its row tombstoned; once premium, the card reads "Premium Β· no limit" and the same create and upload pass
pinned-offline A pinned trip's PDF and its export work offline on a device that never opened them
auto-pin A trip starting in three days keeps itself offline on a device that never opened it nor touched its switch β€” its PDF comes down on its own and its card says so β€” and switched off by hand there it stays off across a relaunch
dots The online dot sits bottom-left on the top bar's avatar: green, red once the backend is cut, green again
dots The sync dot in the menu's header: green synced, red offline, orange while a round is held; hovering it opens nothing, a click opens its popover, Sync now runs a round, a second click or a click outside closes it, Enter opens it and Escape closes it
dots On a touch screen, a tap on the sync dot opens the popover and a tap elsewhere in the panel closes it
dots The panel: the avatar with its sync dot, the name and the email, no role badge, then Profile, Statistics, Users, About and Sign out; Statistics opens the site's page for an admin (the user's own otherwise), and an admin typing the user's page lands on the site's
avatar A JPEG XL picture: cropped to a 512 px JPEG, shown in the top bar, the hero and the panel, kept in the eager cache, painted offline with its controls locked, replaced by a PNG that evicts the old version, removed
toolbar-row The Itinerary and Planner toolbars in the pinned trip bar: same row top and height, first filter pill at the same spot, same bar height β€” at 375, 560, 1280 and 1600 px wide
place-label-row The Google Maps button beside PLACE LABEL is as tall as the input and level with it: the item form's Details, Reservation, Departure and Arrival panels and the Edit idea panel, at 375 and 1280 px wide and on a touch phone
place-search A real place search, backend β†’ gmaps sidecar β†’ Google: "Madrid" typed into the item form's Find a place lists Madrid, and picking it fills the place label (needs Google reachable from the stack)
trip-focus No focus by default: the calendar button unlit, the dates in the header, the panel's three days none picked and Live off; every item in the itinerary, the map's list and pins, every planner column β€” and still so after a reload
trip-focus A picked day: the button lit with its number, the chip naming it; only that day's item in the itinerary, the map's list and pins, its planner column lit among the others β€” kept across a reload; on a phone the planner shows that column alone, the chip's β€Ί steps to the next day (disabled on the last) and βœ• brings the whole trip back
trip-focus Live on a trip from yesterday to tomorrow: yesterday's item leaves the itinerary, the map's list and pins, the planner starts at today, undated items stay; picking a day turns Live off
morning-stay A two-night stay added after the days' 10:00 visits opens the days after its nights with its morning link: none on day 1, "β€ΉHotelβ€Ί Β· morning" at 09:00 – 09:30 heading day 2 (closed by night 2's row) and the check-out day, its πŸ—‘ disabled and its ✎ opening a panel with no Delete; the planner shows it as a block with ✎ alone, the map's list at the same 09:00 – 09:30 as the itinerary row (its To when you leave the hotel), and a picked check-out day keeps the link and its pin beside the visit's, the stay's own pin no longer drawn
stay-links The same kind of two-night stay closes each night with its night link: the itinerary shows each night as the stay itself β€” "night ½", "night 2/2", its ✎ and an enabled πŸ—‘ β€” with the link's 10:45 – 11:15 behind a moon (30 minutes after the day's last entry) opening a "Night link" panel with no Delete, and no row of the link's own; the planner shows the stay as a solid chip and a night block with ✎ alone on each night; the chip dropped on a day outside the stay is refused (not-allowed cursor, no outline, nothing changes) and on one of its days leaves a stopover; the map's list shows only the links and the stopover, never the stay, and a picked day pins the links, the stopover and the visit alone; deleting the stay from a night row takes every link with it
planner-group-move On the planner, Ctrl/⌘-click picks two blocks ("2 selected" in the toolbar, no form opened); dragging one of them a day later and an hour down carries both (the ghost reads "2 items", one outline per block) while a block left out stays put, and the selection outlives the drop, offering "Moved 2 Β· Undo" β€” Undo puts both back; a plain click then picks a third block on the last day, and a drag one more day on keeps every block on its day and moves them all an hour; Escape lets the selection go, and a lasso from the empty hours picks only the block it touches
planner-group-move On a touch phone (another device that pulled the trip), a long press released where it began picks a block ("1 selected", no form opened), a tap then picks a second, a long press dragged an hour down moves them both, and the toolbar's βœ• lets them go
email-wrap At 365 px a long e-mail address breaks only at its @ or before a dot, never inside a word: the Users page's row, the account menu's header and the profile hero
login-scene The sign-in page draws the animated travel scene behind its card, and no login picture: the retired picture is never asked for and its manifest answers 404
login-scene The login theme on the device's clock is the scene's: the night scene (the drawings' night copies, the night's own pieces and their loops) at 20:00, the day at noon
login-scene Reduce motion is the picture's first frame, still: the veil lifted, the plane on its opening pose, no animation
login-scene The activation page's dead link and the email-confirmation card draw the scene too, the card above it
item-form-defaults An empty start date opens filled: the first day of a trip that lies ahead (a new item, an undated item's edit, the map's locked Details once unlocked), today at the current time on a trip under way
item-form-defaults The end date fills from the start as its panel opens β€” a plane's arrival on its departure day, a stay's check-out the next morning β€” follows the start until set by hand, and a stay saved without opening its check-out saves the next morning
item-form-defaults The position button beside Find a place, on a faked GPS fix: an activity's blank label reads "Current location", its Google Maps button opens the coordinates and the pick map pins them; a transport arrival gets the label and keeps its hand-typed Name
plane-connections A plane's Ticket panel adds connections, each new flight prefilled with the flight before's airline and reservation number and every flight headed by its route ("Flight 1 Β· Brest β†’ Marseille"), an added flight's box opening on its heading and bin with its airport's label and Name on one row; removing a flight rejoins the route, the first stop takes the map pick; the save reaches the server, both devices' itinerary rows name every flight number in order, and another device opens the flights in order with their tickets
plane-connections The server keeps connections to planes, whatever a device pushes: a train's connection, a ninth one and a stop off the globe are rejected with its words; a plane's is taken trimmed, its blank stop dropped
plane-connections The locked view (a map pin's Details) reads a plane with a connection flight by flight: each flight one group, its route heading straight over its fields, no stop row between flights, only the set ticket fields shown