π’ 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.
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):
- 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.
- Off-plan or stuck β a review finding outside the plan, or an app still red after two fixing attempts.
- 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 itsdeploy.ps1, which waits until production runs the shipped commit. - The full suite β once shipped, whether to run
fleet-check -Ownnow, every scenario of every own app; its failures land in the rollout's record and inINBOX.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 revertthe platform commit, push (RefApp redeploys), and roll out the fixed version.
β Fleet conformance¶
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.jsonlists the fleet (paths, URLs, local sign-in, the sync collection the credentialed round-trip may use). Each app startsrun-local.ps1 -Detach -ResetDevAdminon its own ports fromfleet.json(its admin signs in with the dev passwordpassword), is checked, then-Stop. Everyrun-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 housenginx.conf, the app's vhost unchanged, onhttps://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 ownrun-local.ps1(since platform 0.27.0) also starts Mailpit on the app's own inbox port fromfleet.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);-Plansays 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 inshared-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/apiroute 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=fullruns 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'sexpectFitsPhone: - 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 -ResetDevAdminblanks 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.ps1audits 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.tsaccounts.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 onlytransformandopacity, on HTML elements or whole<svg>s (waivers named per app inapps.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, itscolor-schemeandtheme-colorwith it, from the first paint;systemfollows 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 |