24 Commits

Author SHA1 Message Date
Sylvain Duchesne 412e5d63e7 Observe the ActivityPods providers instead of asking them what they do
The previous commit treated this package like the polyfill: a provider whose
source is off limits, with the unknowns raised as questions. That was the wrong
regime. The no-read rule attaches to a provider that publishes an engagement —
this one publishes nothing at all, and for that case the protocol has a bounded
reconstruction, made once at adoption time and pinned. So the two questions are
answered here rather than asked, and one of them turned out to be about the Pod
rather than about the providers, which is not our concern.

The observation is now the specification: contract_activitypods-providers,
stamped at the commit it was read from. A record's id is the resource URI and it
is the only identifier, so the seam's two collapse into one. update fetches,
merges and PUTs, so a partial write is safe and the seam declares this backend
as patching.

What the reading also surfaced matters more than either question. Every
container resolution runs against the session's own WebID — in getList, in
create, in a list subscription alike. So this backend can only ever show a user
their own records: no other user's event is reachable, and an event's
participant count cannot be computed from participations the app cannot read.
That is the product, not a detail, so it is need 1 rather than a friction, and
it is phrased as what it is: the capability sits one layer down, exposed as a
WebID parameter, just not reachable through the data provider.

Four frictions are recorded with the workarounds, including the auth provider's
timer and listener that construction installs and nothing removes — which the
runtime backend switch would accumulate, so the interval goes off and the app
checks status itself.
2026-10-02 11:38:23 +02:00
Sylvain Duchesne d48297fe94 Declare what Festipod needs from ActivityPods, rather than guess it
The Refine seam asks four things of a backend, and the ActivityPods README
answers two: how its providers are configured, and that a declared shape tree
gets its own Data Registration and container on the Pod. It is silent on the
record shape and on whether update replaces or patches — the two the seam
cannot default, and the two that fail silently when wrong.

So the declaration is written before adoption instead of after: the surface is
marked as declared rather than measured, the domain conditions are stated at
full strength, and the two unknowns sit in Needs as requests addressed to the
provider. Reading their source to settle them is the one thing this project
does not do.

This branch had no contract manifest at all, so it gets one. It carries no
consume entry, because neither targeted backend publishes an engagement to
pull; the declaration anchors on the README's commit identity, resolved with
ls-remote, which names a state without fetching any content.
2026-10-01 20:14:37 +02:00
Sylvain Duchesne a5f6e01562 Persist the meeting point, and absorb the prefix the generator drops
The meeting point is the product's unit of value and had no shape at all: it
lived in React state, lost on reload. The shape comes from the sibling branch
rather than from my own reading of the domain -- event, host, title, optional
description/place/time, and the optional inbox its own backend will want.

Regenerating the bindings does what that branch documented it would: the
emitted names lose the Fp prefix, because they follow each shape's IRI and
those IRIs are the persisted RDF classes, not ours to rename. So the aliasing
happens at the four import sites, which keeps every downstream name unchanged
and stops the DOM's own Event from being shadowed. The generated files are
never edited; build:orm overwrites them.

I had called this blocked, as "a binding with no consumer". That was wrong and
worth correcting: the shape and the bindings are the PREREQUISITE every
NextGraph-flavoured backend needs, and only the RecordBinding waits for one.
The RecordBindings slot stays optional until a backend can fill it.

Still local-only in the data context: persisting a meeting point needs a
backend to write it through, and the registry is empty.
2026-10-01 19:58:21 +02:00
Sylvain Duchesne 33b4588da4 Remove what the migration left unreachable, and record what it left broken
The shared-wallet shim's fan-out in the data context fed the ORM subscriptions
this provider used to open: readGraphs, writeGraphs, protectedScope,
protectedGraph and the effect that filled them from storeRegistry. Every entity
reads through a Refine backend now, which takes no graph list, so none of it was
reachable. Same for findNg, mapUser and mapParticipation -- reading a record
into a domain type belongs to a backend's binding, not here. storeRegistry
itself stays: the @data multistore scenarios drive it directly.

This is debt the migration created, not debt it inherited, which is why it goes
rather than waiting for a tidy-up.

What the removal COSTS is written where it happened: placement by scope no
longer narrows what the app reads. The backend's visibility declaration is
where that belongs and nothing honours it yet; applyIsolation is the only
narrowing left.

And setLike.delete is now marked as the defect it is. It is a no-op lifted from
a branch where the fixture seed is off, so nothing needs deleting before it --
false here, where the step that empties the wallet therefore does nothing.
Fixing it needs a decision the comment states, because the app exposes no
deleteEvent.
2026-10-01 19:25:17 +02:00
Sylvain Duchesne 0c4f6b6650 Derive the participant count instead of incrementing it
Every Participation is readable by everyone — only the link to a person is
protected — so an event's count is obtained by counting its participations. No
privileged view, nothing to compensate for.

The stored counter is gone from the write path: joining writes a participation
and nothing else, leaving writes nothing else. Writing the participation IS
moving the count, so there is no second write to keep in step and no counter to
race. That race was measured, not supposed: the count sat on whatever the seed
had written, with no error anywhere, because each increment derived from a read
of the previous one.

Derived in BOTH providers, so demo and connected modes cannot disagree about
what a count means. fp:participantCount stays in the shape and the fixtures,
kept in step, but nothing reads it.

The fixtures become the source of truth and had to say something true: they
advertised 24, 12, 45, 16, 30 against five participation rows. Seventeen rows
now, counts aligned to them. Marie's three are untouched on purpose — the home
screen lists "my events" from exactly those, and the @ui tests render against
it. Small on purpose too: a plausible demo, not a hundred rows reproducing
figures nobody reads.

us-7-inscription-evenement.feature still sets a starting count and expects it
±1. Those steps encode the superseded counter and will need rewriting around
creating participations; they are @data, which is parked while no backend is
registered.
2026-10-01 18:35:24 +02:00
Sylvain Duchesne b25bafbb90 Correct what the contract says about the participant count
The previous commit wrote a requirement into types.ts that is false: that the
count needs an inbox, and that a CRUD-only backend therefore cannot carry the
model. That came from misreading the brief's "no filtering" as "the readable
participations are not all of them".

Every Participation is readable by everyone. What is protected is the link to a
person — only a connection can read the profile one points at and recognise who
it is. So the count is obtained by counting an event's participations, which is
ordinary CRUD and asks nothing of a backend.

Left standing, because the measurements did establish it: the count must not be
an incremented field. Reading a stored counter out of a list, adding one and
writing it back derives each write from a read of the previous one, which races
itself — and that mutated counter is the one the project's brief supersedes.

The ng-org backend stays withdrawn, on the reason that holds on its own: it is
not the target, the NextGraph+polyfill provider is.
2026-10-01 16:38:16 +02:00
Sylvain Duchesne 3de86c8be2 Withdraw the ng-org backend, keep the seam that will carry the next two
The participant count is the number of participants INCLUDING those this session
cannot see, so it cannot be a count of the participations we are allowed to
read. The project derives it: whoever signs up deposits a marker into the
event's inbox, and the event OWNER recomputes the field from those deposits.
That needs an inbox. @ng-org/refine-providers is CRUD over SHEX shapes and has
none, so it cannot carry the model — and the counter it could carry, incremented
from a value read back out of a list, races itself and is the one the project's
brief declares superseded. Measured, not assumed: optimistic mutations moved one
assertion from the seeded value to the written one and left the rest.

So the wiring goes and the structure stays. The contract, the registry, the
runtime switch and useBackend are untouched; what is gone is the single entry,
its three ng-org-specific files and the dependency. What the next backend must
answer, the derived count included, is written in types.ts, and the withdrawn
implementation stays in this branch's history as the worked example.

Two refusals rather than quiet degradations. With no backend the data context
falls back to demo data and SAYS so, instead of mounting a connected path that
would hand every screen empty lists. And the @data harness refuses to publish
its bridge at all: on fixtures those scenarios would assert happily and report
green while touching no wallet, which is worse than red.
2026-10-01 16:26:11 +02:00
Sylvain Duchesne 4b4dfa6cad Put every persisted entity on Refine, and the test bridge on the app's view
Profiles and participations now read through useList and write through
useCreate/useUpdate/useDelete, so nothing in the data context talks to the ORM
any more. The seed receives injected writers instead of ORM sets, and decides on
the count the caller has already read -- taken before any await, which is what
the previous version got wrong in the other direction.

The test bridge had to follow. It exposed the raw ORM sets, which no longer hold
what the app holds: a step observing one while the app decides on the other
races itself, and an explicit load seeded a second time because the set still
read empty. The bridge now reads the app's own views through a ref, because a
captured snapshot goes stale after a seed -- the sibling branch had met this and
said so. Its mutations delegate to the data context too; a bridge mutating an
ORM set was testing the bridge.

Those mutations are awaitable now, which is right on its own terms but did NOT
fix what I expected: the counts stay at their seeded values with no error
anywhere, identical before and after. The app's own caveat says the count needs
one connection more than the write, and main only passed because it mutated the
set in place, locally. Those three assertions encode an immediacy this path does
not offer, and whether a test waits for the push is not mine to decide.

STILL FAILING, deliberately left: the three counter assertions above, an
unexplained double seed, and the human wallet-import flow that fails on main
too. setLike.delete is a no-op copied from a branch where nothing needs deleting
before the seed -- true there, false here.
2026-10-01 15:45:55 +02:00
Sylvain Duchesne 974d251079 Rotate the bloated test profile before opening the wallet
The @data suite writes into one wallet that nothing empties, so every run
leaves more behind. Past some volume the broker stops answering in time and
scenarios die on their step timeout, SILENTLY -- nothing in the console. That
failure looks exactly like a bug in whatever was last edited and gets diagnosed
as one: it happened twice in this session before anybody measured the profile,
which had reached 94MB.

Ported from the sibling branch's validate.ts, which already had the remedy and
the threshold. Placed just before ensureAuth rather than at the top of
BeforeAll: that is the branch which actually opens the profile, and the mock
fallback would pay a broker sign-up for nothing.

Two things the comment records rather than hides. Here the rotation resets the
DATA, not just browser state -- nothing serves a shared wallet file in the @data
path, so the wallet is the broker-side one reached through this profile; the
sibling branch documents the opposite, correctly for itself. And it is not free:
a new wallet comes from driving the real broker's sign-up, so each rotation
abandons one there. FESTIPOD_PROFILE_BLOAT_MB=0 opts out.

MEASURED AND UNRESOLVED: one run leaves 54MB, so a 50MB threshold fires every
time. Either it rises or systematic rotation is accepted.
2026-10-01 15:45:41 +02:00
Sylvain Duchesne fd02570b8b Widen the backend contract from one entity to the set
The contract carried a single event binding, which only worked while Events was
the one vertical on Refine. It now carries one binding per persisted entity,
and each binding says which resource name it lives under and who must be able
to read it.

Visibility is DECLARED, not yet enforced: Festipod's confidentiality is
per-entity while Refine has no notion of it, and the NextGraph provider reads
across every graph it can see. Naming it marks where each backend has to close
that gap rather than pretending it is closed.

meetingPoint is optional for now because its SHEX shape does not exist on this
branch; adding it means regenerating the ORM bindings, which drops the Fp prefix
and is its own step. Friendship is absent on purpose — it has no shape and no
persistence anywhere in this project.
2026-10-01 12:29:30 +02:00
Sylvain Duchesne efdfcac4c1 Make the Refine backend a choice rather than a wiring
A backend now answers a contract, and each of its four points is there because
the Events vertical ran into it: how its providers are configured (SHEX shape
types here, resources+jsonContext for ActivityPods), what its records look like,
which key Refine addresses a record by versus which id the app references, and
whether its update replaces or patches. Nothing in it is anticipated.

Switching happens at runtime, so nothing resolves at module scope: the current
backend is React state held by RefineProvider, read through useBackend(), and
<Refine> carries its name as key so switching remounts it and drops the cache
of the backend being left. Leaving one loses access to its data, accepted.

Not done, and not guessed: no activitypods backend. Its configuration is
published, its record shape and update semantics are not, and it declares
resources in ActivityStreams while Festipod persists fp: shapes -- as:Event
exists, a meeting point does not. And AuthGate still gates on the NextGraph
session, so a second backend needs its own barrier to be reachable at all.

The @data budgets in connexion.steps.ts came from ng-eventually, which had
already met this: the timeout was in waitForFunction's SECOND slot, where it is
taken as the predicate's argument, so the wait silently ran on the 30s default.
2026-09-28 16:08:13 +02:00
Sylvain Duchesne 750538bd73 Take the sibling branch's answer for the seed step, instead of a rewrite
ng-eventually lives with one document per entity already, so it had met this
cost and settled it in two lines: await the seed, and give the wait a budget
that matches what opening five documents takes. The 10 s was calibrated when
seeding meant adding rows to an already-open graph.

This replaces a rewrite of the step, begun before looking there, that would
have had each scenario create its own event. @data is now 14 passed / 1 failed,
the same as main, and the remaining failure is main's too.
2026-09-21 16:47:37 +02:00
Sylvain Duchesne 20b3ef8712 Seed only what the tests need, and record that it did not speed anything up
Doctrine prescribes a connected seed that writes only what is needed, because
it is linear in documents. Verified safe before applying: no step asserts on a
seeded participation -- a scenario SETS its starting participant count on the
event and reads it back -- and no profile is looked up by name. So three
profiles instead of fourteen, and no participations.

The speed argument for it was REFUTED by measurement: 24 documents took 13.3s,
eight took 20.2s. Main writes 24 in 1.4s on the same accumulated wallet, so
neither document count nor accumulation is what dominates. Main adds into one
already-open graph; this branch opens a document and a fresh subscription per
event, which is what the Refine provider's create does. The cost is in opening
documents, not in writing triples.

Kept anyway, on the justification that survives: it writes less into a wallet
nothing ever empties.
2026-09-21 15:52:31 +02:00
Sylvain Duchesne ff285455d2 Say where the auto-seed flag can actually arrive
The recovered comment claimed a dev server picks the flag up from the
environment it was built with. It does not: dev and start both serve from src/,
so build.ts's define only reaches the staging bundle the @e2e harness builds.
OFF, the default and the reason the gate exists, needs no plumbing; opting IN
is what is out of reach, and closing that gap means making the app bootstrap
async, which nothing here needs yet.
2026-09-21 14:12:03 +02:00
Sylvain Duchesne 35df3e9110 Decide the seed on the caller's view, and take the sibling branch's answers
bootstrapWallet consulted ngUsers.size, which the await for the subscription
lets the broker re-populate — a caller that has just emptied the wallet then
never seeds again. The count now comes from the caller, as the event count
already did: deciding whether to seed reads what the caller sees, writing
needs the resolved set.

Recovered from ng-eventually, which had answers this session went looking for:
autoSeed.ts, whose gate keeps the dev seed from bloating a persistent wallet
over repeated runs (its fixtureSeedEnabled master switch is deliberately NOT
enforced here — adopting it stands down the seeding scenarios and is a product
decision); overlay-local-checkout.ts, which holds the single-@ng-org invariant;
ng: NG in place of typeof NG, and the harness reading the app's own
currentUserId with optional chaining, which together take typecheck from 3
errors to 1.
2026-09-21 14:08:58 +02:00
Sylvain Duchesne db483726af Install with pnpm, as the sibling branch does
bun install does not rewrite what is already in node_modules when a version
goes DOWN, which silently left @ng-org/orm and @ng-org/web on versions that
disagree about orm_start_graph's arity — the callback then lands in the config
slot and the SDK rejects it as unserializable. Every dependency measurement
taken this way was meaningless.

Carried over from ng-eventually with it: cucumber invoked through the package
entry rather than the .bin shim, which node --import tsx/esm cannot load under
pnpm; the build:orm paths, which still pointed at a src/shapes that moved to
src/shared/shapes; and the tsconfig fix that DROPS baseUrl instead of silencing
its deprecation — reported as a config error, it made tsc exit without checking
a single file.
2026-09-21 13:55:32 +02:00
Sylvain Duchesne 46abbf2e64 Wait for the ORM subscription before seeding into it
The placeholder useShapeSet hands back while a subscription loads refuses
writes, and bootstrapWallet writes its users straight into that set — so the
seed threw on its very first add and the wallet reported nothing persisted.
The refusal is right; issuing the write that early was not.

The previous orm version handed back a writable set immediately for a defined
scope, so a caller never had to wait. Since alpha.21 the set does not exist
until the subscription resolves, so loadTestData now awaits the ORM's own
promise and seeds into the set it resolves with — re-reading ngSet after the
await would hand over the placeholder this closure captured. The dev auto-seed
gates on the same readiness.
2026-09-21 13:53:19 +02:00
Sylvain Duchesne 753282c7b1 Absorb the useShape contract change the provider versions require
@ng-org/orm stopped returning the DeepSignalSet from useShape and now returns
a result object carrying it under data. The bump is forced: the providers
import RdfOrmSubscription, which alpha.18 published as OrmSubscription.

useShapeSet unwraps it in one place and hands back a set, so the call sites
keep the contract they were written against. While the subscription is
loading it yields an empty set that refuses writes rather than dropping
them, matching what the previous version documented.
2026-09-21 12:17:40 +02:00
Sylvain Duchesne d237753916 Mount the @data harness inside Refine, and stop the bundle leaking globals
The harness builds its bundles with a classic script tag, so an unwrapped
bundle publishes every top-level declaration as a global. Bundling Refine
brought in @tanstack/query-core, whose exported MutationObserver class then
shadowed the DOM one for Playwright's injected script, and every
waitForFunction died on defaultMutationOptions. --format=iife keeps the
bundle's declarations to itself.

The harness also mounted the data context outside <Refine> and called the
previous bootstrapWallet signature; it now mirrors the app's provider order
and seeds through the context, so tests and app write events by one path.
2026-09-21 12:13:12 +02:00
Sylvain Duchesne 33d54c6e3e WIP: events read and written through Refine
Mounts Refine over @ng-org/refine-providers and routes the Events vertical
through it. Known incomplete: the @data harness still mounts the data context
outside <Refine> and calls the old bootstrapWallet signature.
2026-09-21 11:58:32 +02:00
Sylvain Duchesne 266e33556d feat(auth): staging wallet partagé — import assisté par fichier + e2e multi-navigateur
Stopgap staging multi-user sur wallet partagé (cf. brief_2026-06-15_shared-wallet-shim).

Distribution / import du wallet :
- AccessGateScreen : barrière d'accès ON PAR DÉFAUT (désactivable via
  globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ pour tests/dev). Fournit le FICHIER
  .ngw + le mot de passe + un guide en 3 étapes (import assisté sur nextgraph.eu —
  le broker hébergé n'autorise pas l'import inline pendant l'auth web-app).
- sharedWallet.ts + build.ts : fichier copié en /shared-wallet.ngw, mot de passe gravé.
- Ancien LoginScreen (/login) retiré ; atterrissage post-login -> /home.
- NextGraphContext : dé-piégeage de l'état "connecting" au retour (pageshow/bfcache).

Couche multistore stopgap : storeRegistry, isolation, AccountContext, FestipodDataContext.

Tests e2e multi-navigateur :
- browserPool + world.openBrowser : contextes frais isolés, 2 axes orthogonaux
  (nb de navigateurs × modèle de wallet own/shared).
- @humain : parcours humain complet (télécharge -> importe le fichier sur
  nextgraph.eu -> Entrer -> pseudo -> accueil).
- Bypass de la barrière pour @e2e via context.addInitScript.
- Convention @wip exclue via cucumber.json.

Docs (concepts) : nextgraph-platform (knowledge_broker-import-constraint,
decision_2026-06-17_assisted-wallet-import), bdd-testing (knowledge_multibrowser-harness).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 12:04:02 +02:00
Sylvain Duchesne 222658a75d docs(concepts): shared-wallet-shim — statut d'implémentation (flags OFF)
Couche compte/login + isolation livrées et vérifiées ; couche multi-document
(storeRegistry) livrée derrière FESTIPOD_MULTISTORE/FESTIPOD_STAGING (OFF par
défaut, mono-store reste le défaut), runtime NG à valider sur broker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:05:55 +02:00
Sylvain Duchesne 0294e3992f docs(concepts): migrate project docs into 7 concepts + code-grounded audit
Migrate .project/{knowledge,decisions,briefs} and the always-loaded
AGENTS.md/CLAUDE.md into the in-repo `concept` system (hook-delivered,
typed leaves). Then audit the actual code to verify the migrated doctrine
and capture knowledge that lived only in the source.

Concepts (53 leaves):
- functional-domain — produit : point de rencontre greffé, acteurs, déduplication
- app-architecture — modules, invariant d'imports, routing, écrans, styling-system,
  screen-pattern, cookbook d'ajout d'écran
- tech-stack — Bun-first, APIs, build pipeline, deployment (Dockerfile), commandes
- data-layer — NextGraph mono-store, shapes, modes, règles + caveats (suppression,
  champs non persistés, internals du contexte)
- bdd-testing — Cucumber multi-couches, contrat de couches, harness, cookbook
- app-security — posture actuelle (mono-store, confiance broker), auth wallet,
  brief matrice d'autorisations cible
- nextgraph-platform — NextGraph système externe + briefs (multi-store, shim, fork)

Audit corrections:
- décision SPARQL-delete annulée (superseded) → caveat (le code utilise ngSet.delete,
  persistance possiblement partielle)
- divergences relevées : routing path-based (pas hash), thème moderne sous components/sketchy,
  ConnectScreen hors registre, build:orm au chemin périmé, champs d'event perdus en connecté

Strip migrated sources; AGENTS.md/CLAUDE.md réduits au cœur (but, invariants,
carte des concepts) + pointeurs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:58:44 +02:00
Sylvain Duchesne 445a448031 docs: NextGraph multi-user data model — stores, auth matrix, inbox fork plan
Capture the multi-user design exploration as project knowledge + briefs:
- knowledge: NextGraph store types/permissions (+ inbox at protocol, SDK
  exposure, local repo path); integration model (iframe, where the verifier
  runs, generic JS plumbing, ngd stateful, build-time broker target)
- briefs: multi-store refactor; authorization matrix + query inventory +
  derived store partitions; temporary fork to expose the inbox (3 layers:
  SDK fork, Coolify self-hosting, Festipod integration; libs via build:ng)
- fix stale @ng-org versions (alpha.11 -> alpha.13) and a broken
  decision-record link in data-layer.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:38:51 +02:00
128 changed files with 12186 additions and 3468 deletions
+5
View File
@@ -41,3 +41,8 @@ playwright/.auth/
*storybook.log
storybook-static
dist-staging/
*.ngw
# per-developer contract source access (never committed)
.project/contracts.local.yaml
-175
View File
@@ -1,175 +0,0 @@
# Matrice d'autorisations et inventaire des requêtes
**Status:** Incubating — analyse en cours
**Last updated:** 2026-05-18
## Context
Préalable au refactor multi-store ([brief](./multi-store-refactor.md)) et à toute évolution multi-user. La structure de stores NextGraph cible doit être *dérivée* de :
1. Une matrice d'autorisations (qui peut faire quoi sur quel type de donnée).
2. Un inventaire des requêtes nécessaires (lectures, abonnements, écritures par écran).
3. Les partitions naturelles qui en découlent (regroupements de données qui partagent autorisations *et* schéma d'accès).
Ce brief porte cette analyse. Il alimentera la décision finale sur la structure de stores.
## Cadre
### Acteurs (tous authentifiés)
- `Self` — propriétaire de la donnée (varie par type : auteur d'un message, titulaire d'un profil…)
- `D` — Déclarant d'un événement (celui qui a inséré la référence dans Festipod ; pas l'organisateur réel)
- `H` — Hôte d'un point de rencontre (celui qui l'a créé)
- `I` — Inscrit à un point de rencontre
- `C` — Connexion (« ami ») d'un autre acteur lié à la donnée
- `U` — Utilisateur authentifié quelconque, sans relation à la donnée
### Verbes
- `créer`
- `lire` (one-shot)
- `s'abonner` (lecture longue / réactive)
- `modifier`
- `supprimer`
### Conventions
`✓` autorisé · `✗` interdit · `cond` autorisé sous condition (notée) · `—` sans objet
## Décisions cadre (acquises)
- **Tous les utilisateurs sont authentifiés.** Pas d'accès anonyme.
- **Points de rencontre publics universels.** Tout utilisateur peut lire et s'abonner.
- **Création de point de rencontre ouverte à tous.** Pas de prérequis (adhésion, invitation).
- **Hôte = détenteur technique des droits d'écriture** sur un point de rencontre. À ce stade : 1 hôte par PdR, celui qui l'a créé.
- **Adhésion à une communauté : hors périmètre actuel.** Le rôle « Membre de communauté » n'est pas analysé ici.
- **Suivi de communauté ou d'utilisateur : hors périmètre actuel.** À reprendre quand la fonctionnalité de discovery par abonnement sera traitée.
## Matrice par type de donnée
### Point de rencontre
| Verbe | Self (= Hôte) | I (autre inscrit) | D (déclarant de l'événement parent) | U (utilisateur lambda) |
|---|---|---|---|---|
| créer | ✓ (l'acte de créer rend l'utilisateur hôte) | — | ✗ | ✓ (l'acte le rend hôte) |
| lire | ✓ | ✓ | ✓ | ✓ |
| s'abonner | ✓ | ✓ | ✓ | ✓ |
| modifier | ✓ | ✗ | ✗ | ✗ |
| supprimer | ✓ | ✗ | ✗ | ✗ |
**Notes :**
- Pas de différenciation `C` (connexion de l'hôte) — les connexions sont un filtre d'affichage côté UI, pas un droit d'accès, puisque tout est public.
- Le `D` n'a pas de droit particulier sur les PdR greffés sur son événement déclaré — il a juste déclaré la référence.
### Inscription à un point de rencontre
L'objet « Inscription » lie un utilisateur et un point de rencontre. Représente l'engagement à participer.
| Verbe | Self (l'inscrit) | H (hôte du PdR) | I (autre inscrit au même PdR) | U (utilisateur lambda) |
|---|---|---|---|---|
| créer | ✓ (s'inscrire) | ✗ | ✗ | ✓ (l'acte le rend inscrit) |
| lire | ✓ | ✓ | ? **à trancher** | ? **à trancher** |
| s'abonner | ✓ | ✓ | ? **à trancher** | ? **à trancher** |
| modifier | ? **à trancher** (selon les champs modifiables) | ✗ | ✗ | ✗ |
| supprimer | ✓ (se désinscrire) | ? **à trancher** (modération ? blacklist ?) | ✗ | ✗ |
**Questions ouvertes :**
- **Visibilité de la liste des inscrits.** Cohérent avec « tout est public » : tous les utilisateurs voient qui s'est inscrit. Mais à confirmer — y a-t-il un cas où on veut cacher la liste (PdR à inscription confidentielle) ?
- **Champs modifiables d'une inscription.** Booléen seul, ou champs additionnels (commentaire, statut "peut-être", nombre d'accompagnants) ?
- **Modération par l'hôte.** L'hôte peut-il désinscrire un inscrit (= blacklist) ?
### Événement
| Verbe | Self (= D, déclarant) | H (hôte d'un PdR greffé) | U (utilisateur lambda) |
|---|---|---|---|
| créer | ✓ (l'acte rend déclarant) | — | ✓ (l'acte le rend déclarant) |
| lire | ✓ | ✓ | ✓ |
| s'abonner | ✓ | ✓ | ✓ |
| modifier | ? **à trancher** | ? **à trancher** | ? **à trancher** |
| supprimer | ? **à trancher** | ✗ | ✗ |
**Questions ouvertes :**
- **Qui peut modifier un événement déclaré ?** Le déclarant seul (modèle propriétaire) ? Tout utilisateur (modèle wiki, pour compléter/corriger) ? Personne après création (modèle immuable, pour éviter les modifications mal intentionnées) ? Cette question est centrale pour le défi de déduplication évoqué dans le README — un modèle wiki facilite la convergence, un modèle propriétaire complique.
- **Qui peut supprimer ?** Si le déclarant supprime, que deviennent les PdR greffés (orphelins ? supprimés en cascade ? l'événement reste mais marqué supprimé ?) ?
### Profil utilisateur
À déterminer : un seul objet ou split public/privé ?
| Verbe | Self | C (connexion) | U (utilisateur lambda) |
|---|---|---|---|
| créer | ✓ (à l'inscription) | — | — |
| lire (partie publique) | ✓ | ✓ | ? **à trancher** |
| lire (partie privée) | ✓ | ? **à trancher** | ✗ |
| s'abonner | ✓ | ? | ? |
| modifier | ✓ | ✗ | ✗ |
| supprimer | ✓ (auto-destruction du compte) | ✗ | ✗ |
**Questions ouvertes :**
- **Split public/privé ?** Le profil contient-il des champs réservés aux connexions ou à l'utilisateur seul (préférences, paramètres, email) ?
- **Profil entièrement public ?** Cohérent avec « points de rencontre publics » : un visiteur peut voir le profil de l'hôte d'un PdR. Mais le détail (bio, photos, ville…) ?
### Connexion (lien d'amitié)
| Verbe | Self (A, demandeur) | Other (B, l'autre côté de la connexion) | U (utilisateur lambda) |
|---|---|---|---|
| créer (demande) | ✓ | — | — |
| accepter | — | ✓ | ✗ |
| lire (sa propre liste d'amis) | ✓ | — | — |
| lire (la liste d'amis d'un autre) | — | — | ? **à trancher** |
| s'abonner (à sa liste) | ✓ | — | — |
| modifier | — | — | — |
| supprimer (rompre la connexion) | ✓ | ✓ | ✗ |
**Questions ouvertes :**
- **Bilatérale ou unilatérale ?** Le concept « connexion / ami » suggère bilatérale (les deux acceptent). À confirmer ; si oui, il y a deux objets distincts : `DemandeDeConnexion` (unilatérale) et `Connexion` (bilatérale).
- **Visibilité de la liste d'amis.** Une connexion est-elle observable par des tiers ? « Marie est connectée à Bob » est-il public, restreint, ou privé ?
## Hors périmètre actuel
À reprendre quand ces concepts deviendront actifs :
- **Communauté d'intérêt** (membres, modération, création)
- **Adhésion à une communauté**
- **Liste curated** (création, partage, abonnement)
- **Suivi d'utilisateur ou de communauté** pour discovery distribuée
## Inventaire des requêtes par écran
*À remplir une fois la matrice des autorisations stabilisée.*
Schéma prévu :
| Écran | Lectures one-shot | Abonnements | Écritures | Acteur déclencheur |
|---|---|---|---|---|
Écrans à analyser (depuis [AGENTS.md](../../AGENTS.md#routing)) :
- `WelcomeScreen` `/`
- `LoginScreen` `/login`
- `HomeScreen` `/home`
- `EventsScreen` `/events`
- `CreateEventScreen` `/events/new`
- `EventDetailScreen` `/events/:id`
- `UpdateEventScreen` `/events/:id/edit`
- `InviteScreen` `/events/:id/invite` (à voir si encore pertinent)
- `ParticipantsListScreen` `/events/:id/participants`
- `MeetingPointsScreen` `/events/:id/meeting-points`
- `ProfileScreen` `/profile`
- `UpdateProfileScreen` `/profile/edit`
- `FriendsListScreen` `/profile/friends`
- `ShareProfileScreen` `/profile/share`
- `UserProfileScreen` `/users/:id`
- `SettingsScreen` `/settings`
## Partitions naturelles dérivées
*À remplir une fois la matrice + l'inventaire stabilisés.*
Heuristique de dérivation : on regroupe dans un même store les données qui (a) partagent leur cellule d'autorisation pour les verbes d'écriture, et (b) sont accédées ensemble dans la majorité des requêtes (pour éviter de multiplier les abonnements).
## See Also
- [Brief : refactor multi-store](./multi-store-refactor.md) — consommateur principal de cette analyse
- [README §Modèle fonctionnel](../../README.md) — source des acteurs et concepts
- [Knowledge : data layer](../knowledge/data-layer.md) — état actuel mono-store
-126
View File
@@ -1,126 +0,0 @@
# Refactor multi-store NextGraph
**Status:** Incubating — aucun travail démarré
**Last updated:** 2026-05-17
## Context
L'app Festipod est aujourd'hui *mono-store* : tout ce que l'app écrit (events, profils, participations, friendships) atterrit dans le `private_store` de l'utilisateur connecté. C'est un héritage du sample expense-tracker-rdf, formalisé dans [la décision du 2026-03-17](../decisions/2026-03-17-1600-private-store-nuri-scope.md).
Ce choix bloque toute évolution vers du multi-utilisateurs : par construction le `private_store` est non partageable (cf. [data-layer](../knowledge/data-layer.md) et la doc NextGraph officielle — *« It is not possible to share the documents of your private store with anybody else »*). Tant que tout est dans le private_store, Bob ne pourra jamais voir l'event d'Alice.
Le modèle natif NextGraph est *multi-store par utilisateur* (private, protected, public, group, dialog) — chaque type d'information a sa place. Festipod doit s'aligner sur ce modèle avant de pouvoir devenir collaboratif.
**Déclencheur :** discussion du 2026-05-17 sur la suite multi-user. Décision prise : *poser le cap, exécuter plus tard*.
## What We Know
### État actuel du code
Deux fichiers concentrent le hardcoding du store unique :
- `src/shared/utils/ngGraph.ts:30` — `ensureGraphNuri()` retourne `did:ng:${session.private_store_id}` pour TOUTES les entités, peu importe leur nature.
- `src/shared/hooks/useShapeWithDefaults.ts` — accepte un `storeNuri` mais l'appelant unique (`FestipodDataContext`) lui passe systématiquement le NURI du private_store.
Entités impactées (toutes mélangées dans le même store aujourd'hui) :
- `FpEvent` — devrait vivre dans un store partagé (logique multi-user)
- `FpUserProfile` — devrait être en partie privée, en partie publique
- `FpParticipation` — liée à un event, devrait vivre avec lui
- `FpMeetingPoint` — actuellement local-only côté types ([`src/shared/data/types.ts:106`](../../src/shared/data/types.ts)), pas encore branché à NextGraph
- `FpFriendship` — actuellement local-only, naturellement privée
### Modèle cible proposé
Structure hiérarchique en **4 niveaux de Group stores** (pas de private/public pour le métier collaboratif — tout en Group) :
```
┌─ Group store « index communautaire » ────────────────────┐
│ Référence tous les events visibles dans la communauté │
│ Lecture par tous les membres, sert d'annuaire/discovery │
│ │
│ ┌─ Group store « communauté » ──────────────────────┐ │
│ │ Propriétaire de l'event │ │
│ │ Permissions = qui peut modifier l'event │ │
│ │ (organisateurs / membres de la communauté) │ │
│ │ │ │
│ │ ┌─ Group store « event » ─────────────────────┐ │ │
│ │ │ Tout ce qui se rattache à l'event : │ │ │
│ │ │ participations, infos pratiques, discu… │ │ │
│ │ │ Membres = participants à l'event │ │ │
│ │ │ │ │ │
│ │ │ ┌─ Group store « meeting point » ───────┐ │ │ │
│ │ │ │ Un RDV de l'event = son propre group │ │ │ │
│ │ │ │ Permet participations + discu │ │ │ │
│ │ │ │ scopées au point de rencontre │ │ │ │
│ │ │ └───────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
Mapping entités → store cible :
| Entité | Store cible | Justification |
|---|---|---|
| Event (métadonnées : titre, dates, description) | Group store « communauté » | C'est la communauté qui possède l'event, donc qui contrôle qui peut le modifier |
| Référence d'event (pointeur depuis l'index) | Group store « index communautaire » | Discovery : « voici les events visibles » |
| Participation | Group store « event » | Une participation n'a de sens que dans le contexte de son event |
| MeetingPoint (métadonnées) | Group store « event » | Le RDV appartient à l'event |
| Participation à un MeetingPoint | Group store « meeting point » | RSVP/présence scopés au RDV |
| UserProfile (partie publique) | public_store de l'utilisateur | Modèle natif NextGraph |
| Friendship | private_store de l'utilisateur | Donnée purement personnelle |
### Contrainte SDK bloquante
La création de Group stores et la gestion des invitations/permissions **ne sont pas exposées dans le SDK `@ng-org/web` actuel** (version `0.1.2-alpha.11`). Les méthodes disponibles : `doc_create`, `doc_subscribe`, `sparql_query/update`, `orm_start_*`, `file_get`, `app_request_stream`. Aucune méthode `share_doc`, `invite_user`, `create_group_store`, `accept_invite`. La doc NextGraph annonce qu'*« An API will be provided for permission manipulation »* — pas de date.
**Implication :** le refactor *structurel* (passer d'un store unique à un système de stores par entité) peut commencer sans attendre cette API, en utilisant des placeholders (par ex. continuer à pointer vers `private_store_id` pour les Group stores qui ne peuvent pas encore exister). Mais l'**aboutissement complet** (vrai multi-user, partage entre wallets distincts) dépend de l'arrivée de l'API SDK ou d'un contournement (fork du wallet, accès Rust direct, etc.).
### Implications côté code
Le refactor touche au moins :
1. **Disparition de `ensureGraphNuri()`** comme helper unique. Remplacé par des helpers par entité (`getEventStore(communityId)`, `getParticipationStore(eventId)`, `getProfileStore(scope: 'public' | 'private')`, …) ou par une couche `storeRegistry` qui résout le NURI selon `(entité, contexte)`.
2. **`useShapeWithDefaults` reste un wrapper utile** mais l'appelant choisit explicitement le store. Aujourd'hui un seul appelant ([`FestipodDataContext`](../../src/shared/context/FestipodDataContext.tsx)), demain N appelants ou un appelant qui résout dynamiquement.
3. **Chaque entité de domaine déclare son store cible** — soit via un mapping centralisé, soit via une convention (shape → store).
4. **`bootstrapWallet()`** ([`src/shared/utils/ngBootstrap.ts`](../../src/shared/utils/ngBootstrap.ts)) doit être revu : on ne seed plus dans un unique store, on doit seed dans plusieurs (ou décider que seed ne crée que des données de l'utilisateur courant — ce qui colle mieux à la réalité multi-user).
5. **`FestipodDataContext`** : structurer les hooks par entité, chacun avec son store résolu.
## Open Questions
1. **Quand crée-t-on un Group store de communauté ?** L'API n'existe pas en SDK aujourd'hui. Faut-il que ce soit un acte explicite de l'utilisateur (« créer une communauté ») ou bien tout user a une communauté par défaut à la création de son wallet ?
2. **Comment Bob connaît-il l'index communautaire d'Alice ?** Discovery toujours ouverte — possiblement via le public_store d'Alice qui annonce le NURI de l'index communautaire.
3. **Faut-il vraiment 4 niveaux d'imbrication ?** Le « meeting point comme group store » mérite d'être validé — quel besoin réel justifie une couche de permission supplémentaire vs un simple sous-graphe du group store de l'event ?
4. **Que devient le seed de démo** quand l'app est multi-store et que les Group stores ne peuvent pas encore exister ? Mode dégradé en private_store le temps que le SDK rattrape, ou retirer le seed en mode connecté ?
5. **Migration des wallets existants** : les wallets de test ont déjà des données dans le private_store. Comment on les fait évoluer (script de migration, wipe and reseed, ignore) ?
6. **Bootstrap d'un user vierge** : à la première connexion, faut-il auto-créer un Group store communautaire « par défaut » pour lui ou attendre une action utilisateur ?
## Possible Approaches
Esquisses sans engagement (les arbitrages se feront dans une décision dédiée au moment de l'exécution) :
- **Refactor structurel d'abord, partage ensuite.** Réorganiser l'app en multi-store dès maintenant en utilisant `private_store_id` comme placeholder pour les Group stores manquants. Quand l'API arrive, on remplace les placeholders par de vrais NURIs de Group stores.
- **Registry centralisé** vs **résolution par convention**. Soit un `storeRegistry.ts` qui mappe explicitement `(entité, contexte) → NURI`, soit chaque shape porte sa propre logique de scope.
- **Big-bang** vs **par entité**. Tout migrer en un coup vs migrer entité par entité (commencer par Event qui est le plus stratégique).
- **Maintenir un mode mono-store** parallèle pour le dev/demo tant que les Group stores ne sont pas fonctionnels.
## Out of Scope
Ce brief — et le refactor qui en découlera — **ne traite pas** :
- L'invitation effective d'utilisateurs à un Group store (capability sharing, Nuri d'invitation)
- La gestion des permissions par rôle (organisateur / membre / lecteur)
- La résolution du problème de discovery cross-wallet
- Le contournement éventuel de l'UI wallet (jugée dysfonctionnelle dans cette conversation)
- Le mode P2P direct sans broker
Ces sujets relèvent d'un **second chantier multi-user** dont le refactor multi-store est seulement le *prérequis structurel*.
## Starting Points
- [decision: private_store NURI scope](../decisions/2026-03-17-1600-private-store-nuri-scope.md) — la décision actuelle qu'on viendra modifier
- [knowledge: data-layer](../knowledge/data-layer.md) — état actuel du pattern d'écriture
- [`src/shared/utils/ngGraph.ts`](../../src/shared/utils/ngGraph.ts) — point de hardcoding principal
- [`src/shared/hooks/useShapeWithDefaults.ts`](../../src/shared/hooks/useShapeWithDefaults.ts) — l'autre point de hardcoding
- [`src/shared/context/FestipodDataContext.tsx`](../../src/shared/context/FestipodDataContext.tsx) — l'unique appelant aujourd'hui
- [`src/shared/utils/ngBootstrap.ts`](../../src/shared/utils/ngBootstrap.ts) — le seed à revoir
- NextGraph docs : [Documents et Stores](https://docs.nextgraph.org/en/documents/), [Getting started](https://docs.nextgraph.org/en/getting-started/)
@@ -0,0 +1,24 @@
---
type: _overview
summary: Architecture feature-based de l'app — modules par domaine, invariant d'imports, app shell à providers, routing path-based, écrans et registre
triggers:
keywords: [module, modules, screen, écran, routing, route, navigate, useNavigate, useParams, registry, registre, app shell, shared, import]
paths: ["src/app/**", "src/screens/**", "src/modules/*/screens/**", "src/shared/components/**", "src/shared/context/**"]
---
# App architecture
Comment le code de l'app est **structuré** et **assemblé**. Architecture *feature-based* : le code est organisé par **domaine métier** (module), pas par couche technique.
**À lire en premier :** [[rule_module-imports]] — l'invariant central qui garde les modules découplés.
## Liens
- [[knowledge_module-structure]] — arborescence modules + couche `shared/`
- [[knowledge_app-shell]] — `src/app/`, pile de providers, points d'entrée
- [[knowledge_routing]] — routing path-based (History API), table de routes, hooks
- [[knowledge_screens]] — inventaire des écrans, registre, lib de composants
- [[knowledge_screen-pattern]] — anatomie canonique d'un écran (sans props, layout flex, showToast)
- [[knowledge_styling-system]] — `src/index.css`, classes `app-*`, vars, pièges (Tailwind non-utilisé, `user-content` inerte)
- [[cookbook_add-screen]] — procédure pour câbler un nouvel écran (registre + router + shell)
- `tech-stack` — build, bundler Bun, commandes
@@ -0,0 +1,20 @@
---
type: cookbook
summary: Procédure pour ajouter un écran — créer le composant dans le module, l'enregistrer dans src/screens/index.ts, ajouter la route dans router.tsx, le monter dans App.tsx, et un alias screenNameMap si testé en BDD
---
# Cookbook : ajouter un écran
Un écran doit être câblé à **plusieurs endroits** — en oublier un produit des bugs silencieux (cf. le cas `ConnectScreen`, [[knowledge_screens]]).
1. **Créer le composant** : `src/modules/{module}/screens/MyScreen.tsx`, en suivant [[knowledge_screen-pattern]] (fonction sans props, `useFestipodData`/`useNavigate`/`useParams`, layout flex, style via [[knowledge_styling-system]]). Respecter [[rule_module-imports]] (importer seulement depuis `shared/`).
2. **Enregistrer dans le registre** : `src/screens/index.ts` — ajouter l'import + l'entrée (`id`, `name` FR, `path`, `component`). **Étape la plus oubliée** : un écran absent du registre est invisible à Storybook et aux consommateurs du registre, même s'il fonctionne en route.
3. **Ajouter la route** : `src/app/router.tsx` — étendre le type `Route`, ajouter le cas dans `parsePath()` (et la conversion inverse si présente).
4. **Monter dans le shell** : `src/app/App.tsx` — ajouter le cas dans le switch qui mappe `route.page` → composant.
5. **(Si testé en BDD)** : ajouter un alias dans `screenNameMap` (`src/shared/steps/ui/navigation.steps.ts`) si le nom français du `.feature` ne se résout pas trivialement vers l'`id`. Voir concept `bdd-testing`.
> Vérifier la cohérence : l'`id` doit être identique entre le registre, le router et `screenNameMap`. Un écart silencieux = écran injoignable ou non rendu.
@@ -0,0 +1,33 @@
---
type: knowledge
summary: src/app/ est le shell réel de l'app — App.tsx empile les providers (Theme > NextGraph > FestipodData > Router) et bascule l'écran selon la route
---
# App shell
`src/app/` est le **shell de l'app réelle** (mobile web app), pas un outil de prototypage.
> Note de migration : d'anciennes notes décrivaient `src/app/` comme un « prototyping tool » en routing par hash (`#/`, `#/demo/...`). C'est **périmé** depuis la restructuration en vraie app. La vérité courante : routing path-based via History API (voir [[knowledge_routing]]).
## Pile de providers
`App.tsx` empile les providers puis bascule l'écran selon la route courante :
```
ThemeProvider
└ NextGraphProvider (cycle de connexion NextGraph — concept data-layer)
└ FestipodDataProvider (données, mode connected/demo — concept data-layer)
└ RouterProvider (route courante + navigate)
```
Le composant racine lit `useRouter()` pour résoudre `route.page` → écran à rendre.
## Points d'entrée
| Fichier | Rôle |
|---|---|
| `src/index.ts` | `Bun.serve()` — serveur HTTP, sert `index.html` + rapport cucumber |
| `src/index.html` | Entrée HTML, charge `src/app/frontend.tsx` |
| `src/app/frontend.tsx` | Racine React, rend `<App />` |
Le build et le bundler (Bun + Tailwind, alias `@/* → ./src/*`) sont documentés dans le concept `tech-stack`.
@@ -0,0 +1,41 @@
---
type: knowledge
summary: Arborescence feature-based — modules métier (event, user, home, auth, workshop, meeting, notification) et couche shared/ importable par tous
---
# Structure des modules
Le code est organisé par **domaine métier**, pas par couche technique.
```
src/modules/
event/ # Événements : CRUD, discovery, participants, points de rencontre
user/ # Profils, connexions (« amis »), partage
home/ # Dashboard, settings
auth/ # Login, welcome/onboarding
workshop/ # Specs atelier (features seulement, pas d'écrans)
meeting/ # Specs point de rencontre (features seulement)
notification/ # Specs notification (features seulement)
```
Chaque module peut contenir :
- `screens/` — composants d'écran React
- `features/` — fichiers Gherkin `.feature` (specs BDD, voir concept `bdd-testing`)
- `steps/{ui,data,e2e}/` — step definitions Cucumber par couche
## Couche `shared/`
`src/shared/` contient tout le réutilisable inter-modules :
| Répertoire | Contenu |
|---|---|
| `components/` | Lib de composants UI (voir [[knowledge_screens]]) |
| `context/` | `ThemeContext`, `NextGraphContext`, `FestipodDataContext` (voir concept `data-layer`) |
| `data/` | User stories, `features.ts` (auto-généré), `seedData.ts`, `types.ts` |
| `hooks/` | `useShapeWithDefaults` (NextGraph) |
| `shapes/` | SHEX + bindings ORM (voir concept `data-layer`) |
| `utils/` | `ngSession.ts`, `ngBootstrap.ts`, `ngGraph.ts` |
| `steps/`, `support/` | Step definitions et hooks Cucumber partagés (concept `bdd-testing`) |
| `lib/` | Helpers (`cn`, etc.) |
La règle de dépendance entre modules et `shared/` est dans [[rule_module-imports]].
@@ -0,0 +1,36 @@
---
type: knowledge
summary: Routing path-based via History API (router maison dans src/app/router.tsx) — table de routes, hooks useNavigate/useParams, pas de prop drilling
---
# Routing
Routing **path-based** via l'History API — router maison dans `src/app/router.tsx` (`window.history.pushState` + `popstate`, `parsePath(pathname)`). Pas de routing par hash.
## Table de routes
| Path | Écran |
|---|---|
| `/` | WelcomeScreen |
| `/login` | LoginScreen |
| `/home` | HomeScreen |
| `/events` | EventsScreen |
| `/events/new` | CreateEventScreen |
| `/events/:id` | EventDetailScreen |
| `/events/:id/edit` | UpdateEventScreen |
| `/events/:id/invite` | InviteScreen |
| `/events/:id/participants` | ParticipantsListScreen |
| `/events/:id/meeting-points` | MeetingPointsScreen |
| `/profile` | ProfileScreen |
| `/profile/edit` | UpdateProfileScreen |
| `/profile/friends` | FriendsListScreen |
| `/profile/share` | ShareProfileScreen |
| `/profile/connect` | (connexion) |
| `/users/:id` | UserProfileScreen |
| `/settings` | SettingsScreen |
> Cette table reflète `parsePath()` dans `router.tsx` — y revenir si elle évolue, c'est la source de vérité.
## Hooks
Les écrans utilisent `useNavigate()` et `useParams()` du router — **pas de prop drilling**. Le shell intercepte la navigation pour basculer l'écran affiché (voir [[knowledge_app-shell]]).
@@ -0,0 +1,43 @@
---
type: knowledge
summary: Anatomie canonique d'un écran — fonction nommée sans props, lit tout via useFestipodData/useNavigate/useParams, layout flex colonne (Header / contenu scrollable / BottomNav pour les écrans hub), feedback via showToast, libellés français en dur
---
# Pattern canonique d'un écran
Tous les écrans suivent la même forme. La connaître évite de réinventer ou de diverger.
## Forme
```tsx
export function MyScreen() { // fonction nommée, JAMAIS de props
const navigate = useNavigate();
const { eventId, userId } = useParams();
const { getEvent, currentUser, … } = useFestipodData();
const [local, setLocal] = useState(…); // état local d'écran (étapes, sélections)
const handleAction = () => {
// …muter via useFestipodData
showToast('Message', 'success'); // feedback
navigate('/path');
};
return (
<div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
<Header title="…" /* left/right optionnels */ />
<div style={{ flex:1, overflow:'auto' }}>{/* contenu scrollable */}</div>
<BottomNav active="…" /> {/* seulement sur les écrans hub */}
</div>
);
}
```
## Invariants
- **Zéro prop** : l'écran ne reçoit rien ; tout vient du contexte/hooks (`useFestipodData`, `useNavigate`, `useParams`). Exceptions légitimes : `LoginScreen`/`WelcomeScreen` n'utilisent pas `useFestipodData` (auth/intro).
- **Layout** : flex colonne pleine hauteur ; `Header` en haut, contenu en `flex:1; overflow:auto`, `BottomNav` en bas **uniquement pour les écrans hub** (Home, Events, Profile, Friends). Les écrans de flux (création, édition, détail) n'ont pas de `BottomNav`.
- **Feedback** : `showToast(message, 'success'|'info'|'error')` (mécanisme `ToastContainer` exporté par `sketchy/`).
- **Libellés** : **français, en dur** — aucun i18n, aucune clé de traduction dans le projet.
- Style : voir [[knowledge_styling-system]]. Navigation/registre : [[knowledge_routing]], [[knowledge_screens]].
Pour **créer** un écran (les 3+ endroits à câbler), voir [[cookbook_add-screen]].
@@ -0,0 +1,40 @@
---
type: knowledge
summary: Inventaire des écrans par module, registre central src/screens/index.ts, et lib de composants sous shared/components/sketchy/ — dont le NOM est conservé mais qui rend un thème moderne (pas hand-drawn)
---
# Écrans et composants
## Lib de composants : `sketchy/` = thème moderne
⚠️ **Piège de nommage.** La lib de composants vit sous `src/shared/components/sketchy/` (chemin conservé, importé par ~17 écrans), **mais elle ne rend plus un style « hand-drawn »** : elle a été portée vers un thème **moderne** (DM Sans / orange, classes `app-*`). Le *chemin d'import* est bon, la *description visuelle « sketchy »* est périmée. Ne pas réintroduire d'esthétique dessinée en se fiant au nom du dossier.
Composants typiques : `Header`, `BottomNav`, `Button`, `Card`, `Input`, `Badge`, `Avatar`/`AvatarStack`, `Text`/`Title`, `Toggle`, `ListItem`, `Divider`, `Placeholder`, `BrokerBanner`, `NgStatus`.
## Registre d'écrans
`src/screens/index.ts` importe tous les écrans de tous les modules et expose :
```typescript
export const screenGroups // groupés par domaine (home, events, user, general)
export const screens // liste à plat
export function getScreen(id): Screen | undefined
```
Utilisé notamment par Storybook (voir concept `tech-stack`) pour parcourir les écrans.
## Inventaire
Écrans par module (IDs = clés du registre) :
- **home/** : `welcome`, `home`, `settings`
- **event/** : `events`, `event-detail`, `create-event`, `update-event`, `invite`, `participants-list`, `meeting-points`
- **user/** : `profile`, `update-profile`, `user-profile`, `friends-list`, `share-profile`
- **auth/** : `login`
> Le mapping path → écran est dans [[knowledge_routing]]. La plupart des écrans consomment `useFestipodData()` (concept `data-layer`) ; exceptions : `LoginScreen`/`WelcomeScreen`.
## Piège : registre incomplet
Le registre doit lister **tous** les écrans. Cas observé : `ConnectScreen` (`src/modules/user/screens/`, routé `/profile/connect`, monté dans `App.tsx`) est **absent de `src/screens/index.ts`** → invisible à Storybook et aux consommateurs du registre, bien qu'il fonctionne en route. Toujours vérifier que l'écran est enregistré (cf. [[cookbook_add-screen]]).
@@ -0,0 +1,32 @@
---
type: knowledge
summary: src/index.css est la source de vérité du style — variables --app-* (couleurs, rayons, police DM Sans) et classes app-* rendues par les composants ; les écrans combinent ces classes avec des styles inline ; Tailwind est dans le build mais les écrans n'utilisent pas d'utilitaires Tailwind ; la classe user-content est inerte
last_checked: 2026-06-15
---
# Système de style
**Source de vérité : `src/index.css`** (thème « Modern clean — DM Sans »). C'est là que vivent les variables CSS et les classes `app-*`. Pas de fichiers CSS par module.
## Variables (`:root`)
- Couleurs : `--app-black #1a1a1a`, `--app-gray #888`, `--app-bg/--app-white #fff`, accent orange `--app-accent #E8590C` (+ `-light #FFF7ED`, `-border`, `-dark #C05621`), vert `--app-green #22543D` (+ `-light`, `-border`, `-text`).
- Rayons : `--app-radius 16px`, `--app-radius-sm 12px`, `--app-radius-xs 8px`.
- Police : `--font-app: 'DM Sans', …`.
## Classes `app-*`
Définies dans `index.css`, rendues par les composants de `shared/components/sketchy/` : `app-btn` (+ `-primary`/`-green`), `app-input`, `app-card`, `app-title`/`app-subtitle`/`app-text`, `app-badge`, `app-toggle`, `app-checkbox`, `app-header`, `app-navbar`, `app-list-item`, `app-avatar`, `app-placeholder`, `app-divider`, `app-tab`.
## Conventions d'écriture d'un écran
- Utiliser les **composants `sketchy/`** (qui portent les classes `app-*`) pour boutons/inputs/cartes/typo.
- Pour le **layout** (flex, gaps, paddings, couleurs ponctuelles), les écrans utilisent des **styles inline** (`style={{…}}`) — c'est le pattern normal, pas une déviation.
- Icônes : **emojis**/symboles Unicode (📅 📍 📝 🎪…), pas d'imports d'icônes en général.
- Largeur : `.app-container` borne à **`max-width: 768px`, `height: 100dvh`** (mobile-first/tablette portrait). Aucune media query — pas de responsive desktop.
## Pièges
- **Tailwind est dans le build** (plugin `bun-plugin-tailwind`, dépendance `tailwindcss`), mais **les écrans n'utilisent pas de classes utilitaires Tailwind** — le style réel passe par `app-*` + inline. Ne pas « tailwindiser » un écran en pensant suivre la convention.
- **`user-content` est une classe INERTE** : utilisée sur de nombreux titres/noms dans les écrans, **sans aucune définition CSS**. C'est un marqueur legacy sans effet — ne pas s'appuyer dessus pour styler, ne pas croire qu'elle fait quelque chose.
- Pas de **dark mode** : le toggle « darkMode » de `SettingsScreen` n'est branché à rien.
@@ -0,0 +1,24 @@
---
type: rule
summary: Un module n'importe QUE depuis shared/ (et le registre d'écrans) — jamais depuis un autre module ; c'est l'invariant qui garde l'architecture feature-based
---
# Règle : un module n'importe jamais d'un autre module
**Les modules importent uniquement depuis `shared/` — jamais entre eux.**
```
src/modules/event/screens/EventDetailScreen.tsx
✅ import depuis 'shared/components/...'
✅ import depuis 'shared/context/FestipodDataContext'
✅ import depuis 'src/screens' (types du registre)
❌ import depuis 'modules/user/screens/...'
```
## Pourquoi
C'est ce qui rend l'architecture *feature-based* réelle et pas cosmétique : chaque domaine reste un bloc autonome, déplaçable/supprimable sans casser les autres. Tout besoin partagé **remonte dans `shared/`** ; toute dépendance inter-domaines passe par un contrat de `shared/` (souvent `FestipodDataContext` ou le registre d'écrans), jamais par un import direct.
## Vérifier
`grep -rE "from '\.\./\.\./(event|user|home|auth|workshop|meeting|notification)/" src/modules/` ne doit rien remonter d'un module vers un *autre* module. Un import qui croise deux noms de modules différents est une violation.
@@ -0,0 +1,23 @@
---
type: _overview
summary: Sécurité & confidentialité de Festipod — posture ACTUELLE (mono-store, confiance broker, aucun contrôle d'accès côté app) et modèle d'autorisations CIBLE (incubation) ; authentification par wallet NextGraph
triggers:
keywords: [sécurité, security, confidentialité, privacy, accès, "access control", contrôle d'accès, trust, confiance, authz, autorisation, permission, wallet, auth, authentification, anonyme, identité, login]
paths: ["src/modules/auth/**", "src/shared/context/NextGraphContext.tsx"]
---
# App security
Le modèle de **sécurité, confidentialité et autorisations** de Festipod. Le pilier se lit en deux temps :
- **Actuel** — ce que le code applique aujourd'hui : voir [[knowledge_trust-model]]. Résumé brutal : **aucun contrôle d'accès côté app**, l'app affiche le `private_store` de l'utilisateur connecté et fait confiance au broker. Mono-user de fait.
- **Cible** — le modèle d'autorisations dérivé (qui peut faire quoi, données personnelles = réseau, anonymat via inbox) : [[brief_2026-05-18_authorization-matrix]]. **Incubation, non implémenté.** Il graduera en `rule_`/`behavior_` quand le multi-user atterrira (chantiers data dans le concept `nextgraph-platform`).
L'écart entre les deux est volontaire : tant que l'app est mono-store (cf. concept `data-layer`), il n'y a rien à autoriser côté app.
## Liens
- [[knowledge_trust-model]] — posture de sécurité actuelle (mono-store, confiance broker, pas d'enforcement app)
- [[knowledge_authentication]] — auth par wallet NextGraph, tous authentifiés, pas d'accès anonyme
- [[brief_2026-05-18_authorization-matrix]] — modèle d'autorisations cible (incubation)
- `nextgraph-platform` — les primitives (stores, capabilities, inbox) et les chantiers data qui porteront la cible
@@ -0,0 +1,143 @@
---
type: brief
summary: Matrice d'autorisations par type de donnée (PdR, inscription, événement, profil, connexion) ; dérive que 3 stores natifs par utilisateur + Dialog stores suffisent, aucun Group store sur le périmètre validé ; questions ouvertes sur modèle d'écriture événement et identité de l'hôte
last_updated: 2026-05-18
---
# Matrice d'autorisations et inventaire des requêtes
**Status:** Incubating — analyse en cours
**Last updated:** 2026-05-18
## Context
Préalable au refactor multi-store ([[brief_2026-05-17_multi-store-refactor]]) et à toute évolution multi-user. La structure de stores NextGraph cible doit être *dérivée* de : (1) une matrice d'autorisations ; (2) un inventaire des requêtes par écran ; (3) les partitions naturelles qui en découlent (données partageant autorisations *et* schéma d'accès).
C'est aussi le **modèle de confidentialité/sécurité** de Festipod (pilier sécurité), non encore implémenté.
## Cadre
### Acteurs (tous authentifiés)
`Alice` (point de vue, propriétaire de la donnée en focus) · `Bob` (second protagoniste, relations bilatérales) · `D` (déclarant d'événement) · `H` (hôte d'un PdR) · `I` (inscrit) · `C` (connexion) · `U` (utilisateur lambda sans relation).
### Verbes
`créer` · `lire` (one-shot) · `s'abonner` (lecture réactive) · `modifier` · `supprimer`. Conventions : `✓` autorisé · `✗` interdit · `cond` sous condition · `—` sans objet.
## Décisions cadre (acquises)
- **Tous authentifiés.** Pas d'accès anonyme.
- **Points de rencontre publics universels.** Tout utilisateur peut lire et s'abonner.
- **Création de PdR ouverte à tous.** Pas de prérequis.
- **Hôte = détenteur des droits d'écriture** sur un PdR (1 hôte, le créateur ; le fait d'être hôte est public).
- **Informations personnelles = réservées au réseau.** Visibles seulement au titulaire et à ses connexions : participations, intégralité du profil, liste de connexions, et tout état déclaratif dont la divulgation serait une fuite. Statut « public » (PdR, événement) et « personnel » (profil, participations, connexions) coexistent dans le même utilisateur.
- **Connexion bilatérale.** Existe après acceptation des deux côtés. Deux objets : `DemandeDeConnexion` (unilatérale, transitoire) et `Connexion` (bilatérale, persistante).
- **Notification d'inscription via l'inbox NextGraph du PdR.** L'acte « s'inscrire » est composite : (a) écriture d'un objet `Inscription` dans le `protected_store` de l'inscrit, (b) dépôt d'un lien (DID cap) dans l'**inbox** du document PdR. Identification du sender par résolution du DID contre le graphe de connexions de l'hôte : connexion → inscription complète visible ; sinon → lien opaque (« quelqu'un (DID…) s'est inscrit »). Anonymat partiel **natif aux capabilities** (cf. [[knowledge_stores-permissions]] §Inbox).
- **Adhésion à une communauté / suivi : hors périmètre actuel.**
## Matrice par type de donnée
### Point de rencontre
| Verbe | Alice (= Hôte) | I (autre inscrit) | D (déclarant parent) | U (lambda) |
|---|---|---|---|---|
| créer | ✓ (rend hôte) | — | ✗ | ✓ (rend hôte) |
| lire | ✓ | ✓ | ✓ | ✓ |
| s'abonner | ✓ | ✓ | ✓ | ✓ |
| modifier | ✓ | ✗ | ✗ | ✗ |
| supprimer | ✓ | ✗ | ✗ | ✗ |
Notes : pas de différenciation `C` (les connexions sont un filtre d'affichage UI, pas un droit, tout étant public). Le `D` n'a aucun droit particulier sur les PdR greffés sur son événement.
### Inscription à un point de rencontre
`Inscription` lie un utilisateur et un PdR. **Donnée personnelle** (inscrit + ses connexions). Acte composite (a)+(b) ci-dessus.
| Verbe | Alice (inscrite) | C (connexion) | H (hôte) | I (autre inscrit) | U |
|---|---|---|---|---|---|
| créer (acte composite) | ✓ | — | ✗ | ✗ | ✓ (rend inscrite) |
| lire le contenu | ✓ | ✓ | cond : ✓ si H ∈ connexions(Alice) ; sinon lien opaque | cond : ✓ si I ∈ connexions(Alice) | ✗ |
| s'abonner | ✓ | ✓ | cond (idem) | cond (idem) | ✗ |
| lire l'inbox du PdR (entrées brutes) | — | — | ✓ | ✗ | ✗ |
| modifier | ? **à trancher** (selon champs) | ✗ | ✗ | ✗ | ✗ |
| supprimer | ✓ (se désinscrire ; retirer le lien de l'inbox si possible) | ✗ | cond : modération inbox seule (ne supprime pas l'objet) | ✗ | ✗ |
**Visibilité hôte : résolue** (identifiée si connecté, anonyme sinon — natif). **Questions ouvertes :** champs modifiables d'une inscription (booléen seul ou +commentaire/statut/accompagnants ?) ; **suppression côté inbox** — un déposant peut-il retirer son lien d'un doc qu'il ne contrôle pas ? (à vérifier au protocole).
### Événement
| Verbe | Alice (= D) | H (hôte d'un PdR greffé) | U |
|---|---|---|---|
| créer | ✓ (rend déclarant) | — | ✓ (rend déclarant) |
| lire / s'abonner | ✓ | ✓ | ✓ |
| modifier | ? **à trancher** | ? **à trancher** | ? **à trancher** |
| supprimer | ? **à trancher** | ✗ | ✗ |
**Questions ouvertes :** qui peut **modifier** un événement déclaré — déclarant seul (propriétaire) ? tout utilisateur (wiki) ? personne (immuable) ? Central pour la déduplication (cf. concept `functional-domain`, [[brief_2026-06-15_event-deduplication]] côté functional-domain). Qui peut **supprimer**, et que deviennent les PdR greffés (orphelins/cascade/marqué supprimé) ?
### Profil utilisateur
**Rien dans le profil n'est public.** Deux périmètres : **profil réseau** (Alice + connexions : nom, avatar, bio, ville, intérêts) ; **profil privé** (Alice seule : settings, email, préférences).
| Verbe | Alice | C | U |
|---|---|---|---|
| créer | ✓ (à l'inscription) | — | — |
| lire — réseau | ✓ | ✓ | ✗ |
| lire — privé | ✓ | ✗ | ✗ |
| s'abonner | ✓ | ✓ (réseau) | ✗ |
| modifier | ✓ | ✗ | ✗ |
| supprimer (compte) | ✓ | ✗ | ✗ |
**Tension à résoudre :** un PdR est lisible par tous, mais son hôte ne devrait pas être identifiable par un lambda. Trois positions : (i) **pseudonyme par DID seul** (nom/avatar résolus seulement aux connexions) ; (ii) **identité dénormalisée dans l'offre** (l'hôte choisit une « carte de visite » par PdR, vivant dans l'objet PdR, profil fermé) ; (iii) **anonymat de l'hôte** (identité révélée seulement aux connexions). À trancher. Autres : composition champ-par-champ de chaque périmètre ; statut du `username` (public/réseau/supprimé ?).
### Connexion (lien d'amitié)
Bilatérale. `DemandeDeConnexion` (unilatérale, en attente) → `Connexion` (bilatérale, à l'acceptation ; ouvre l'accès aux données personnelles). La liste de connexions d'Alice est **personnelle** (Alice + ses connexions).
| Verbe | Alice (initiatrice) | Bob (autre côté) | C | U |
|---|---|---|---|---|
| créer la demande | ✓ | — | — | — |
| accepter | — | ✓ | — | ✗ |
| lire la liste d'Alice | ✓ | ✓ | ✓ | ✗ |
| s'abonner | ✓ | ✓ | ✓ | ✗ |
| supprimer (rompre A↔B) | ✓ | ✓ | ✗ | ✗ |
**Questions ouvertes :** granularité côté Bob (voit-il toute la liste d'Alice ou juste A↔B ? — conséquence du principe : toute la liste) ; découvrabilité « amis d'amis » (Alice voit-elle Bob↔Carole ? — non, sauf si Carole ∈ connexions(Alice)).
## Partitions naturelles dérivées
Heuristique : même store si (a) même cellule d'autorisation en écriture *et* (b) accédées ensemble. À partir des seuls points validés, **trois périmètres** émergent — qui correspondent **presque parfaitement aux 3 stores natifs**.
| Périmètre | Écriture | Lecture | Données validées |
|---|---|---|---|
| **Public** ↔ `public_store` | Alice seule | Tous | PdR hébergés par Alice ; événements déclarés *(sous réserve du modèle d'écriture)* |
| **Réseau** ↔ `protected_store` | Alice seule | Alice + connexions | Profil réseau ; participations ; index des connexions |
| **Privé** ↔ `private_store` | Alice seule | Alice seule | Profil privé (settings, email, préférences) |
### Cas particulier : la Connexion bilatérale
Donnée à *deux* écrivains → ne tient dans aucun store individuel. Primitive native : le **Dialog store**. Modèle : **une `Connexion` A↔B = un Dialog store** (contient l'objet + matière à messagerie future) ; l'**index « toutes les connexions d'Alice »** vit dans le `protected_store` d'Alice (liste les NURIs des Dialog stores). La `DemandeDeConnexion` : soit dans un Dialog store provisoire, soit dans le `public_store` du destinataire (à trancher selon le SDK).
### Inbox du document PdR
Le doc PdR (dans le `public_store` de l'hôte) a une **inbox** native : reçoit les dépôts d'inscription (liens DID cap), plus tard commentaires/signaux. **Pas un store séparé**, attribut du document. Pas d'impact sur les partitions.
### Ce qui ne demande aucun Group store
Sur le périmètre validé, **aucune donnée ne demande de Group store**. Tout tient dans : 3 stores natifs par utilisateur + Dialog stores + inboxes natives. Les Group stores ne deviennent nécessaires que si le modèle d'écriture événement est « wiki », ou si communautés/suivi/collaboration multi-hôte reviennent dans le périmètre.
### Implication pour [[brief_2026-05-17_multi-store-refactor]]
Ce brief y propose une structure à 4 niveaux de Group stores. **Cette analyse dérive une structure différente** (3 stores natifs + Dialog, sans Group) parce que les concepts qui justifient les Group stores ont été mis hors périmètre. À reconcilier à l'exécution.
## Inventaire des requêtes par écran
*À remplir une fois la matrice stabilisée.* Schéma prévu : `| Écran | Lectures one-shot | Abonnements | Écritures | Acteur déclencheur |`. Écrans à analyser : voir la table de routes (concept `app-architecture`).
## See Also
- [[brief_2026-05-17_multi-store-refactor]] — consommateur principal
- [[brief_2026-06-15_shared-wallet-shim]] — stopgap reprenant ces périmètres
- `README.md §Modèle fonctionnel` / concept `functional-domain` — source des acteurs
- Concept `data-layer` — état actuel mono-store
@@ -0,0 +1,20 @@
---
type: knowledge
summary: Authentification = possession d'un wallet NextGraph ; tous les utilisateurs sont authentifiés (pas d'accès anonyme) ; l'auth passe par le redirect/iframe broker, et l'app n'auto-connecte que dans l'iframe
---
# Authentification
**L'identité d'un utilisateur = son wallet NextGraph.** Il n'y a **pas d'accès anonyme** à l'app : tout utilisateur est authentifié (cf. concept `functional-domain`). Il n'y a pas de système de comptes/mots de passe applicatif — l'auth est déléguée à NextGraph.
## Flux
- `LoginScreen` (`src/modules/auth/screens/`) déclenche la connexion via `useNextGraph()` (ne consomme pas `useFestipodData`).
- Le flux standard `@ng-org/web` est un **redirect vers le broker** (`nextgraph.net/redir/`) qui recharge l'app dans une **iframe** après authentification (détail dans concept `nextgraph-platform`, [[knowledge_integration-model]] côté nextgraph-platform).
- **L'app n'auto-connecte que dans l'iframe broker** (`window.self !== window.top`) — sinon `initNgWeb()` redirigerait toute la page. Cette règle vit côté data-layer ([[rule_conditional-ng-init]]) car elle concerne le cycle `NextGraphContext`, mais elle a une conséquence sécurité directe : **hors iframe, aucune session n'est ouverte sans action explicite** de l'utilisateur.
## Le wallet de test
Les tests `@data`/`@e2e` créent/ouvrent un wallet réel (`festipod-tests`/`festipod-tests`, profil persistant) — voir concept `bdd-testing`. Ce sont des **credentials de test en clair**, sans enjeu de sécurité, dédiés au staging (cohérent avec la posture « utilisateurs amicaux » du stopgap, concept `nextgraph-platform`).
> Le modèle d'autorisations qui s'appuiera sur cette identité (connexions bilatérales, données personnelles = réseau, anonymat hôte) est en incubation : [[brief_2026-05-18_authorization-matrix]].
@@ -0,0 +1,21 @@
---
type: knowledge
summary: Posture de sécurité actuelle — aucun contrôle d'accès côté app, l'app lit/affiche le private_store de l'utilisateur connecté et fait confiance au broker NextGraph pour ne retourner que des données autorisées ; mono-user de fait
last_checked: 2026-06-15
---
# Modèle de confiance actuel
**Posture observée dans `src/shared/context/FestipodDataContext.tsx` (`useNgData`) :** l'app lit tout ce que les subscriptions ORM retournent depuis le `private_store` de l'utilisateur connecté et l'affiche **sans aucun filtre d'autorisation côté app**.
Conséquences (à connaître avant de raisonner sécurité) :
1. **Aucun contrôle d'accès applicatif.** Pas de vérification « l'utilisateur a-t-il le droit de voir cette donnée ». L'app suppose que **le broker/NextGraph ne retourne que ce que l'utilisateur peut voir**. Toute la confidentialité repose sur cette confiance dans la couche NextGraph, pas sur du code Festipod.
2. **Mono-store, donc mono-user de fait.** Tout (events, profils, participations) vit dans le `private_store` de l'utilisateur connecté (cf. concept `data-layer`, [[decision_2026-03-17_private-store-nuri-scope]] côté data-layer). Un autre utilisateur ne voit rien — par construction, le `private_store` n'est pas partageable. Il n'y a donc rien à « autoriser » : chacun ne voit que ses propres données.
3. **Pas de séparation de périmètres.** Le découpage public / réseau / privé du modèle cible ([[brief_2026-05-18_authorization-matrix]]) **n'existe pas encore** dans le code : aucun `protected_store`/`public_store` n'est utilisé pour le métier.
## Le piège pour la suite
Le jour où le multi-user arrive (lecture cross-wallet, voir les briefs de `nextgraph-platform`), cette **absence d'enforcement applicatif devient un risque** : si la séparation reste portée seulement par la crypto/capabilities NextGraph et que l'app continue d'afficher « tout ce qu'elle reçoit », une fuite de capability = une fuite de données. Le stopgap `shared-wallet-shim` (concept `nextgraph-platform`) prévoit d'ailleurs un **filtre d'isolation applicatif** explicite parce que, dans ce mode, un seul wallet rend tout physiquement lisible.
> À vérifier si on doute : `useNgData` dans `FestipodDataContext.tsx` ne contient aucune branche de filtrage par identité ; les seuls IDs manipulés sont ceux du wallet courant.
@@ -0,0 +1,36 @@
---
type: _overview
summary: BDD Cucumber/Gherkin en français sur 3 couches (@ui, @data, @e2e) — setup, contrat de couches (quoi tester où), harness broker réel, et le piège des vestiges source-grep
triggers:
keywords: [cucumber, gherkin, bdd, feature, scenario, scénario, step, steps, "@ui", "@data", "@e2e", playwright, broker, harness, wallet, world, hooks, renderHelper, multibrowser, multi-navigateur, "@multibrowser", "@private-wallet", "@shared-wallet", storageState, "@wip"]
paths: ["src/modules/*/features/**", "src/modules/*/steps/**", "src/shared/steps/**", "src/shared/support/**", "src/shared/test-harness/**", "cucumber.json"]
---
# BDD testing
Tests BDD **Cucumber/Gherkin en français** (`Etant donné`, `Quand`, `Alors`) sur **3 couches** de coût croissant.
**À lire avant d'écrire un test :** [[rule_test-layer-contracts]] — chaque couche répond à une question distincte ; mélanger produit des tests fragiles. C'est la règle qui décide *où* va une assertion.
## Les 3 couches
```
/\ @e2e app réelle dans l'iframe broker — parcours critiques
/ \
/----\ @data mutations & persistance via broker NextGraph réel
/------\
/ @ui \ rendu d'écran in-process (happy-dom + seed) — le gros du volume
/__________\
```
## Liens
- [[rule_test-layer-contracts]] — quoi tester à chaque couche (le contrat)
- [[knowledge_cucumber-setup]] — config, layout, scripts, fichiers auto-générés
- [[knowledge_ui-layer]] — couche `@ui` : render helper, fixtures, bons/anti patterns
- [[knowledge_data-layer-broker]] — couche `@data` : harness broker, cycle de vie wallet, bridge
- [[knowledge_e2e-layer]] — couche `@e2e` : app réelle dans l'iframe
- [[knowledge_multibrowser-harness]] — plusieurs navigateurs isolés × modèle de wallet (private/shared), injection storageState
- [[decision_2026-03-12_headless-wallet-creation]] — pourquoi le wallet de test est créé en UI headless
- [[caveat_source-grep-vestiges]] — vestiges de l'ère « analyse de source » dans `world.ts`
- [[cookbook_add-scenario]] — ajouter un scénario/step (couches, piège de sérialisation `evaluate`, `@wip`)
@@ -0,0 +1,21 @@
---
type: caveat
summary: world.ts garde des vestiges de l'ère « analyse de source » (screenFileMap, screenFieldDetectors, screenExpectedContent, screenRequiredFields ; hasText/hasField/hasElement à fallback source) — à supprimer une fois la migration @ui vers le DOM rendu terminée
last_checked: 2026-06-15
---
# Caveat : vestiges d'analyse de source dans `world.ts`
La suite `@ui` **précède** le contrat de couches ([[rule_test-layer-contracts]]). Des restes de l'ère « grep sur le code source » subsistent et **ne doivent pas être étendus** :
- `world.ts:screenFileMap`, `screenFieldDetectors`, `screenExpectedContent`, `screenRequiredFields` — mappings de l'approche analyse-de-source.
- `hasText` / `hasField` / `hasElement` — **préfèrent désormais le DOM rendu** mais **retombent sur la source** pour que les steps non migrés continuent de marcher pendant la transition.
## Plan de migration (en cours)
1. Réécrire les assertions grep-source → requêtes DOM via le render helper.
2. Supprimer les tests sur détails d'implémentation (`/showDuplicateWarning/`, `/importableEvents/`, regex sur JSX).
3. Déplacer les assertions comportementales vers `@e2e` quand pas déjà couvertes.
4. Retirer les checks de contenu `@e2e` redondants avec `@ui`.
Une fois la migration terminée, les 4 maps vestiges peuvent disparaître au profit d'assertions sur le DOM rendu + seed. **Tant qu'elles existent, ne pas s'appuyer dessus pour de nouveaux tests.**
@@ -0,0 +1,29 @@
---
type: cookbook
summary: Procédure pour ajouter un scénario/step BDD — .feature français taggé, steps par couche, piège de sérialisation de appFrame.evaluate (passer les args, pas de closure), ajouter les helpers aux DEUX harness, tag @wip pour le non-implémenté
---
# Cookbook : ajouter un scénario / un step
1. **Écrire le `.feature`** : `src/modules/{module}/features/us-N-slug.feature`, `# language: fr`, tag de tête `@CATEGORIE @priority-N`, et un tag de couche par scénario (`@ui` / `@data` / `@e2e`). Mots-clés FR : `Fonctionnalité`, `Contexte` (Background), `Scénario`, `Étant donné`/`Quand`/`Alors`. Tagger `@wip` un scénario dont les steps ne sont pas encore écrits.
2. **Choisir la couche** (cf. [[rule_test-layer-contracts]]) : assertion de rendu → `@ui` ; mutation/persistance → `@data` ; parcours complet → `@e2e`.
3. **Écrire les steps** dans `src/modules/{module}/steps/{ui,data,e2e}/*.steps.ts` (ou `src/shared/steps/ui/` si cross-domaine). Signature : `async function (this: FestipodWorld, …)`. Importer `FestipodWorld` depuis `../../../../shared/support/world` (ajuster le chemin relatif).
4. **Accès aux données selon la couche** :
- `@ui` : `this.renderedDoc` / `this.getDomText()` / `this.hasText(...)` après `navigateTo(...)` (voir [[knowledge_ui-layer]]).
- `@data`/`@e2e` : `await this.appFrame!.evaluate(fn, ...args)` sur le bridge `window.__testData` (voir [[knowledge_data-layer-broker]]).
5. **⚠️ Piège de sérialisation `appFrame.evaluate`** : la fonction passée s'exécute **dans l'iframe**, les variables du step **ne sont pas capturées** (closures perdues). **Passer toute valeur en argument** :
```ts
// ❌ const title = eventTitle; await appFrame.evaluate(() => td.getEventByTitle(title)) // title undefined
// ✅ await appFrame.evaluate((t) => td.getEventByTitle(t), eventTitle)
```
Toujours `await` (oublier → assertion avant résolution).
6. **Si tu ajoutes une opération de données** : exposer le helper sur `window.__testData` dans **les deux** harness (`src/shared/test-harness/harness.tsx` ET `harness-ng.tsx`) — sinon le fallback mock diverge du broker réel.
7. **Câbler un écran testé** : si le nom français de l'écran ne se résout pas vers son `id`, ajouter un alias dans `screenNameMap` (`src/shared/steps/ui/navigation.steps.ts`).
8. **Lancer** : `bun run test:cucumber` (tout) ou `bun run test:data` (@data). Rapport : `reports/cucumber-report.html`. Le `@data`/`@e2e` exige le wallet de test (`bun run test:auth-setup` au premier coup si besoin, sinon création auto — cf. [[decision_2026-03-12_headless-wallet-creation]]).
@@ -0,0 +1,37 @@
---
type: decision
summary: Décision 2026-03-12 — créer le wallet de test en automatisant l'UI broker headless (Playwright) plutôt que par API NG, car ça teste le vrai flux d'auth et évite de reverse-engineer l'API d'inscription
---
# Automated Headless Wallet Creation for CI
**Date:** 2026-03-12 15:00
**Status:** Accepted
## Context
Les tests `@data` exigent un wallet NextGraph dans un profil Chromium persistant. Avant, le premier run exigeait une interaction manuelle (navigateur visible, création de wallet à la main) → bloquait le CI.
## Options Considered
### Option A: création programmatique du wallet via SDK NG
Appeler `ng.wallet_create()` depuis Node/Bun, sans UI.
- **Pour** : plus rapide, pas de navigateur.
- **Contre** : `@ng-org/web` est browser-only (WASM + postMessage) ; il faudrait reverse-engineer l'API d'inscription d'`account.nextgraph.eu` ; ne teste pas le vrai flux d'auth.
### Option B: automatiser le flux UI headless
Piloter via Playwright la même UI de création de wallet, en headless.
- **Pour** : teste le vrai flux auth/login de bout en bout ; pas de reverse-engineering ; même profil persistant réutilisé ; CI-ready sans étape manuelle.
- **Contre** : dépend de `nextgraph.eu`/`account.nextgraph.eu` joignables ; fragile aux changements d'UI NextGraph ; +~27s au premier run.
## Decision
**Option B** — automatiser l'UI broker. Le flux de création (navigate → Create Wallet → ToS → username/password → submit) est lui-même un test légitime de la feature d'auth. La dépendance aux services externes est acceptable puisque les tests dépendent déjà du broker joignable.
## Consequences
**Positif :** tests pleinement CI-ready (zéro interaction) ; flux auth testé en passant ; `bun run test:data` part d'un état propre.
**Négatif :** exige un accès internet (nextgraph.eu, account.nextgraph.eu) ; fragile aux changements d'UI NextGraph (textes de boutons, IDs de formulaire).
**Risque :** rate-limiting d'`account.nextgraph.eu` si le CI recrée souvent des wallets.
> Mécanique de cycle de vie détaillée : [[knowledge_data-layer-broker]].
@@ -0,0 +1,46 @@
---
type: knowledge
summary: Config Cucumber (cucumber.json, langue fr, loader tsx), layout des features/steps colocalisés par module, steps partagés dans shared/steps/, et les scripts qui génèrent features.ts/testResults.ts/stepDefinitions.ts
---
# Setup Cucumber
26 fichiers `.feature` (US-1 à US-26), tous en **français**, taggés `@CATEGORIE @priority-N` (catégories EVENT, WORKSHOP, USER, MEETING, NOTIF).
## Layout
Features et steps **colocalisés avec leur module** :
```
src/modules/event/features/us-13-creer-evenement.feature
src/modules/event/steps/{ui,data,e2e}/
```
Steps **partagés** (cross-domaine) dans `src/shared/steps/ui/` :
- `navigation.steps.ts` — navigation, auth, clics/sélections, assertions section/bouton/champ
- `form.steps.ts` — validation de champs, champs requis, import/duplicate
- `screen.steps.ts` — contenu d'écran (participants, events, profils, QR)
Les noms français des écrans (`"accueil"`, `"détail événement"`, `"mon profil"`…) mappent vers les IDs d'écran via `screenNameMap`.
Tags de scénario : `@ui` / `@data` / `@e2e` (couche) + **`@wip`** pour un scénario dont les steps ne sont pas encore implémentés. Un `Contexte` (Background) fréquent — « Étant donné que je suis connecté » — ne fait que poser un flag `isAuthenticated`, pas d'auth réelle en `@ui`.
## Config
`cucumber.json` : `import` de `src/shared/support/**`, `src/shared/steps/**`, `src/modules/*/steps/**` ; `paths` = `src/modules/*/features/**`; `language: fr`. **Runner = Node + tsx** (`node --import tsx/esm node_modules/.bin/cucumber-js`), pas Bun — les plugins (Playwright, happy-dom) ne chargent pas en import Bun natif. Ne pas « bunifier » `cucumber:run`/`test:data`.
## Le harness de test est buildé à la demande
Les harness `@data`/`@e2e` (`src/shared/test-harness/harness.tsx`, `harness-ng.tsx`) **ne sont pas** buildés par `build.ts`. Le `BeforeAll` de `hooks.ts` les compile **à la demande** (`bun build` → `dist/test-harness*.js`). Le wallet de test peut être créé d'avance via `bun run test:auth-setup` (`scripts/setup-test-auth.ts`), sinon il est créé automatiquement au premier run (cf. [[decision_2026-03-12_headless-wallet-creation]]).
## Fichiers auto-générés
Des scripts `scripts/` parsent features/steps en data TS consommée par l'outil de parcours :
| Script | Entrée | Sortie |
|---|---|---|
| `parse-features.ts` | `*/features/*.feature` | `src/shared/data/features.ts` |
| `parse-test-results.ts` | `reports/cucumber-report.json` | `src/shared/data/testResults.ts` |
| `extract-step-definitions.ts` | `shared/steps/ui/*.ts` | `src/shared/data/stepDefinitions.ts` |
Lancer : `bun run test:cucumber` (tout), `bun run test:data` (@data). Après ajout de steps : `bun run steps:extract`.
@@ -0,0 +1,36 @@
---
type: knowledge
summary: Couche @data — Playwright pilote Chromium (profil persistant) qui s'authentifie au broker NextGraph réel chargeant harness-ng.tsx en iframe ; cycle de vie wallet automatisé (création + login bootstrap), bridge window.__testData, fallback mock
---
# Couche `@data` (broker réel)
`@data` teste le **vrai pipeline NextGraph** via un broker, pas des données mockées.
## Architecture
```
Cucumber → Playwright (Chromium, profil persistant)
→ broker wallet login (automatisé)
→ broker charge le harness en iframe (http://127.0.0.1:{port})
→ harness-ng.tsx (init → useShape → ORM → broker)
→ bridge window.__testData
```
**Dual mode** : broker réel (`harness-ng.tsx`, défaut) ou fallback mock (`harness.tsx`, DeepSignalSets standalone si le build NG échoue).
## Cycle de vie du wallet (automatisé, CI-ready)
- **Premier run** : pas de marker `.wallet-ready` → Chromium headless crée le wallet (`nextgraph.eu` → Create Wallet → ToS sur `account.nextgraph.eu` → username/password → submit), **puis se logge** — ce login déclenche le bootstrap du verifier depuis le broker distant (peuple `self.repos`, sauvé en localStorage). **Sans ce login initial, toutes les écritures échoueraient en `RepoNotFound`.** Marker écrit.
- **Runs suivants** : marker trouvé → login automatisé (click Login → wallet → password → submit) → harness en iframe → `window.__testData.ready`.
- Credentials wallet : `festipod-tests` / `festipod-tests`.
> Le choix « automatiser l'UI headless plutôt que créer le wallet par API » est tranché dans [[decision_2026-03-12_headless-wallet-creation]].
## Détails techniques
- **Flags Chromium** (`--disable-web-security`, `--allow-insecure-localhost`, désactivation de Private Network Access) : nécessaires car le broker public charge un harness `http://127.0.0.1` en iframe.
- **Profil persistant** `.playwright-profile/` (gitignored, wallet en localStorage) — exige le vrai binaire Chrome, pas `chrome-headless-shell`.
- **Serveur HTTP** lancé en `BeforeAll` (port auto), sert le HTML + `/harness.js` (fichiers séparés — le script inline casse à cause de caractères spéciaux du bundle).
- **Subscriptions ORM** : les 3 shapes avec scope `did:ng:${session.private_store_id}` (cf. concept `data-layer`).
- **Bridge `window.__testData`** : `events`/`users`/`participations` (sets live), `currentUserId`, lookups (`getEvent`, `getEventByTitle`), mutations (`joinEvent`, `leaveEvent`, `updateEvent`), requêtes (`isParticipating`, `getEventParticipants`).
@@ -0,0 +1,47 @@
---
type: knowledge
summary: Couche @e2e — Playwright boote l'app RÉELLE (pas un harness) dans l'iframe broker, interagit via appFrame.evaluate()/locator(), réutilise setupBrokerPage() de @data ; teste navigation/redirects/clics, pas de fallback mock
---
# Couche `@e2e` (app réelle)
`@e2e` teste l'**UI de l'app réelle** tournant dans l'iframe broker — contrairement à `@data` qui charge un harness de test.
## Architecture
```
Cucumber → Playwright (Chromium, profil persistant)
→ https://nextgraph.net/redir/#/?o=http://127.0.0.1:{appPort}
→ login broker (automatisé, même mécanique que @data)
→ broker charge la VRAIE APP en iframe
→ app rend avec NextGraphProvider auto-connectant
→ steps via appFrame.evaluate() + locators Playwright
```
**Serveur app** : lancé en `BeforeAll` (`spawn('bun', ['src/index.ts'], { env: { PORT } })`, poll jusqu'à réponse HTTP, tué en `AfterAll`). Réutilise le helper `setupBrokerPage()` de `@data` (redirect, login, découverte de l'iframe).
## Step definitions
Dans les modules (ex. `src/modules/auth/steps/e2e/connexion.steps.ts`) :
- `this.appFrame!.evaluate()` — JS dans l'iframe app (navigation hash/path, checks de contenu)
- `this.appFrame!.locator()` — éléments DOM
- `this.appFrame!.waitForFunction()` — poll d'état attendu
- `SCREEN_MARKERS` — map ID d'écran → texte unique de vérification
Navigation : `window.history.pushState` + dispatch `popstate` (routing path-based, cf. `app-architecture`).
## Différences avec `@data`
| Aspect | `@data` | `@e2e` |
|---|---|---|
| Chargé en iframe | harness (`harness-ng.tsx`) | app réelle (`src/index.ts`) |
| Signal ready | `window.__testData.ready` | `root.innerHTML.length > 100` |
| Interaction | bridge `evaluate()` | `evaluate()` + locators |
| Fallback mock | oui | **non** (broker réel requis) |
| Teste | opérations données | comportement UI (nav, redirects, clics) |
> **Ne pas re-vérifier en `@e2e` ce que `@ui` couvre déjà** — `@e2e` doit casser quand la *collaboration* entre couches casse, pas quand une icône change (cf. [[rule_test-layer-contracts]]).
## Fichiers clés
`src/shared/support/hooks.ts` (lifecycle Playwright), `world.ts` (champs `page`/`appFrame`), `scripts/debug-browser.ts` (debug headed), `.playwright-profile{,-debug}/` (gitignored).
@@ -0,0 +1,71 @@
---
type: knowledge
summary: Harness multi-navigateur sur DEUX axes orthogonaux — nombre de navigateurs (machinerie, contextes frais isolés via un freshBrowser non-persistant) ET modèle de wallet (own/@private-wallet vs shared/@shared-wallet) ; shared provisionné par injection storageState (test) ; e2e @humain qui valide le mécanisme produit RÉEL via la vraie app staging (fichier .ngw téléchargé depuis l'écran → import nextgraph.eu « Import a Wallet File » → Entrer → connecté) ; convention @wip exclue via cucumber.json
last_checked: 2026-06-16
---
# Harness multi-navigateur (private-wallet vs shared-wallet)
Capacité du harness `@data`/`@e2e` à piloter **plusieurs navigateurs isolés** dans un même scénario, sous **deux axes orthogonaux**. Sert à tester le stopgap wallet partagé (cf. concept `nextgraph-platform` → `brief_2026-06-15_shared-wallet-shim`) **et** le modèle cible (chacun son wallet).
## Les deux axes (orthogonaux)
| Axe | Ce qu'il décide | Exprimé par |
|---|---|---|
| **Nombre de navigateurs** (machinerie) | 1..N contextes nommés isolés | `openBrowser(name, …)` + steps `… dans le navigateur "X"` |
| **Modèle de wallet** | identité NG distincte vs partagée | **phrasing du step + tag** (voir ci-dessous) |
Ne **pas** confondre `@multibrowser` (plusieurs navigateurs) avec `@shared-wallet` (même wallet) : on fait du multibrowser **en private** (utile dès que NextGraph livrera la lecture cross-wallet — le modèle cible) **et en shared** (stopgap), et on compare les deux setups avec les **mêmes** steps de comportement.
## Modèle de wallet : phrasing + tags
- `Étant donné un navigateur "A" avec son propre wallet` → modèle **own**, tag `@private-wallet`.
- `Étant donné un navigateur "A" avec le wallet partagé` → modèle **shared**, tag `@shared-wallet`.
- Tag umbrella `@multibrowser` (feature entière).
## Architecture (où vit quoi)
- **`src/shared/support/browserPool.ts`** — état partagé + fabrique. Hors du contexte Chromium **persistant** porteur du wallet partagé (legacy mono-navigateur `@data`/`@e2e`, **inchangé**, cf. [[knowledge_data-layer-broker]]), le harness lance un navigateur **non-persistant** `freshBrowser` (`chromium.launch`) qui mint des contextes frais et isolés à la demande (`spawnContext(wallet)`). Module importé par `hooks.ts` (cycle de vie) et `world.ts` (usage par scénario) — pas de cycle d'import.
- **`world.ts`** — API : `openBrowser(name, wallet)`, `browser(name)`, `loadAppInBrowser(name, 'app'|'harness')`, `closeBrowsers()` ; registre `browsers: Map<name, NamedBrowser>`. Navigateurs nommés fermés en `After`, `freshBrowser` en `AfterAll`.
- **`hooks.ts`** — un scénario taggé `@multibrowser` **ne reçoit pas** la page unique legacy ; les steps ouvrent les navigateurs. Exige le mode broker réel (`freshBrowser` indispo en fallback mock).
## Provisioning du wallet
- **own** : `newContext()` vide → identité NG distincte / pas de wallet.
- **shared** : `newContext({ storageState })`, où `storageState` est **capturé une fois** au `BeforeAll` depuis le profil persistant (warm-up via `setupBrokerPage` puis `browserContext.storageState()`), exposé par `pool.sharedWalletState`. **Vérifié empiriquement (2026-06-16)** : les origines `nextgraph.eu` + `nextgraph.net` round-trippent dans les contextes frais, et deux navigateurs **shared** atteignent tous deux l'app **connectée** à NextGraph (`window.__testData.ready`) **sans login manuel**.
> Ce provisioning est **de test** — distinct du mécanisme **produit** (import assisté par FICHIER). Le scénario shared-wallet par storageState **court-circuite l'import** ; pour valider le mécanisme RÉEL, voir l'e2e `@humain` ci-dessous.
## Parcours humain — e2e du mécanisme produit (vert)
Scénario `@humain` : valide le flux RÉEL de distribution du wallet **de bout en bout, via la vraie app**, pas l'injection de test (cf. concept `nextgraph-platform` → `decision_2026-06-17_assisted-wallet-import`). Un navigateur vierge ouvre l'app staging → l'`AccessGateScreen` propose le **fichier** + le **mot de passe** → on télécharge le fichier **depuis l'écran**, on vérifie que le mot de passe affiché **égale** celui du wallet → import sur `nextgraph.eu` « Import a Wallet File » → retour → clic « Entrer » → app connectée (`ConnexionScreen`).
- **Wallet e2e** : un fichier `.ngw` (`festipod-e2e-tests`, mot de passe = identifiant) placé **à la racine du worktree** ; `findE2eWalletFile()` le localise (`*.ngw`). Gitignoré → chaque environnement doit l'ajouter (sinon erreur claire).
- `pool.ensureStagingApp()` (`hooks.ts`) — build **isolé** `bun run build.ts --outdir=dist-staging` (barrière d'accès **ON par défaut** ; mot de passe gravé + **fichier copié** en `/shared-wallet.ngw`, cf. `build.ts`), servi statiquement. Mémoïsé, lazy (seul `@humain` le paie).
- **Bypass de la barrière pour `@e2e`** : le harness fait `browserContext.addInitScript` sur le **contexte persistant** pour poser `globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ = true` (s'applique à l'iframe app avant ses scripts) → `@e2e` voit l'app directement, pas la barrière. Les contextes frais (`@humain`) n'y touchent pas → barrière ON (cf. `decision_2026-06-17`). L'ancien `LoginScreen` `/login` a été retiré.
- `pool.importWalletViaFile(page, filePath, password)` — `nextgraph.eu/#/wallet/login` → `setInputFiles('input[type=file]')` (attendre que la SPA rende, sinon `EncryptionError`) → champ password → unlock.
- `pool.completeBrokerLogin(page, appUrl, walletPassword?)` — moitié « login broker » extraite de `setupBrokerPage`. **Attente robuste** : après le redirect (multi-hop), attend l'iframe app OU le lien « Click here to login with your wallet », puis déverrouille avec le mot de passe. La session broker n'étant **pas** persistée entre lancements, ce login wallet est requis à chaque run (warm-up + `@e2e` + `@humain`).
> **C'est l'e2e qui garantit que ça marche pour un humain réel** : Festipod fournit le BON fichier + mot de passe, et ce fichier importé donne un wallet fonctionnel sur un device vierge. Le scénario `@shared-wallet` (storageState) reste un raccourci de provisioning de test, il ne valide pas l'import.
## Isolation (garantie à 3 niveaux, prouvée par les scénarios)
1. `freshBrowser` est un **process séparé** du profil persistant porteur du wallet → un navigateur **own** démarre **sans wallet**.
2. Chaque `newContext()` est une **partition de stockage hermétique** (garantie Playwright).
3. Isolation prouvée non seulement sur l'origine **locale** (`127.0.0.1`) mais aussi sur l'**origine broker** `nextgraph.net` **où vit réellement le wallet** (sonde localStorage écrite dans A absente de B).
## Fichiers
- Feature : `src/modules/workshop/features/multibrowser-harness.feature`.
- Steps : `src/modules/workshop/steps/data/multibrowser.steps.ts`.
- Route `/blank` ajoutée au serveur harness (`hooks.ts`) : page minimale **sans stack NG**, pour les checks d'isolation localStorage.
## Convention `@wip` (désormais appliquée)
`cucumber.json` (profile `default`) porte `"tags": "not @wip"`. Le `cookbook_add-scenario` prescrivait `@wip` pour le non-implémenté mais ce n'était **exclu nulle part** ; maintenant `not @wip` s'**AND** avec les filtres CLI (ex. `--tags @data` → `(not @wip) and @data`, vérifié).
## Liens
- [[knowledge_data-layer-broker]] — la couche `@data` mono-navigateur (profil persistant) que cette capability étend.
- [[cookbook_add-scenario]] — convention `@wip`, pièges de steps.
- Concept `nextgraph-platform` → `brief_2026-06-15_shared-wallet-shim` — le stopgap wallet partagé que ce harness sert à tester.
@@ -0,0 +1,33 @@
---
type: knowledge
summary: Couche @ui — renderHelper.tsx rend tout écran dans LocalDataProvider + happy-dom, world.renderCurrentScreen() l'invoque à chaque navigateTo, assertions sur le DOM rendu avec les fixtures de seed déterministes
---
# Couche `@ui`
`@ui` rend un écran avec `LocalDataProvider` (seed) + `RouterProvider` via happy-dom, puis assert sur le **DOM rendu**.
- Helper : `src/shared/test-harness/renderHelper.tsx` (installe les globals happy-dom, enveloppe l'écran). Invoqué depuis `world.ts:renderCurrentScreen()` à chaque `navigateTo(...)`.
- Fixtures déterministes (`src/shared/data/seedData.ts`, voir concept `data-layer`) : `Marie Dupont`/`@mariedupont` = currentUser, `Jean Durand`/`@jeandurand` existe, 5 events, etc.
## Bons patterns d'assertion
```ts
// Texte visible
expect(this.getDomText()).to.include('Marie Dupont');
// Présence d'élément par classe/rôle
expect(this.renderedDoc!.querySelector('.app-avatar')).to.not.be.null;
// Rendu conditionnel (rempli vs vide)
expect(this.renderedDoc!.querySelectorAll('.app-card').length).to.be.greaterThan(0);
// Champ requis rendu avec label + astérisque
const labels = Array.from(this.renderedDoc!.querySelectorAll('p')).map(p => p.textContent ?? '');
expect(labels.some(t => t.includes("Nom de l'événement *"))).to.be.true;
```
## Champs & helpers de `FestipodWorld` (`src/shared/support/world.ts`)
- `renderedDoc: Document | null` — le DOM happy-dom rendu (peuplé par `renderCurrentScreen()`, appelé à chaque `navigateTo(...)`).
- `currentScreenId: string | null` — l'écran courant.
- Helpers d'assertion : `getDomText()` (texte du DOM), `hasText(t)`, `hasField(name)`, `hasElement(selector)` — ils **préfèrent le DOM rendu** mais **retombent sur la source** des écrans pour les steps non migrés (vestige, voir [[caveat_source-grep-vestiges]]).
> Les classes `app-*` confirment le thème moderne (cf. `app-architecture`). Les anti-patterns (regex sur source, détails d'implémentation) sont proscrits par [[rule_test-layer-contracts]]. Pour écrire un nouveau scénario, voir [[cookbook_add-scenario]].
@@ -0,0 +1,29 @@
---
type: rule
summary: Chaque couche BDD répond à une question distincte — @ui = rendu (DOM + seed), @data = mutations/persistance broker, @e2e = collaboration des couches sur un parcours ; descendre chaque assertion à la couche la plus basse qui peut y répondre
---
# Règle : contrat des couches de test
Chaque couche répond à **une question distincte**. Mélanger les préoccupations produit des tests fragiles qui cassent au refactor sans attraper de vraie régression. **Descendre toute assertion à la couche la plus basse qui peut y répondre.**
- **`@ui` — couche affichage.** Rend un écran avec `LocalDataProvider` (seed) + happy-dom et assert sur le DOM. Vérifie que *données connues → l'écran montre le texte et les éléments attendus*. **Ne teste pas** la navigation, les mutations, ni la persistance.
- **`@data` — couche données.** Pilote des mutations ORM via le **broker NextGraph réel** (harness headless, pas d'UI app). Vérifie que *les opérations sur shapes sont persistées et observables dans le wallet*. Pas de DOM ici — utiliser le bridge `window.__testData`.
- **`@e2e` — couche intégration.** Boote l'app réelle dans l'iframe broker (Playwright/Chromium). Vérifie que *les couches collaborent pour livrer un parcours* (créer → lister → modifier → recharger → toujours là). **Rare** : 1 scénario par chemin critique ; **ne jamais dupliquer** un check de contenu `@ui`.
## Pourquoi le coût impose la pyramide
`@ui` tourne in-process (instantané) ; `@data` boote un broker (~50s) ; `@e2e` boote broker + app + navigateur (~2min). Une affirmation de rendu appartient à `@ui`, pas à `@e2e`.
## Anti-patterns `@ui` à proscrire
```ts
// ❌ regex sur la source : couple le test à la structure du code
expect(/<Title[^>]*>Marie Dupont<\/Title>/.test(source)).to.be.true;
// ❌ détails d'implémentation
expect(/showDuplicateWarning/.test(source)).to.be.true;
```
Préférer des assertions sur le **DOM rendu** + données de seed (voir [[knowledge_ui-layer]]). Les helpers/maps d'analyse de source sont des vestiges en voie de suppression : [[caveat_source-grep-vestiges]].
+35
View File
@@ -0,0 +1,35 @@
---
type: _overview
summary: Couche données NextGraph telle qu'utilisée AUJOURD'HUI (mono-store) — stack ORM/SHEX, modes connected/demo, entités, seed, et 3 règles d'écriture critiques
triggers:
keywords: [nextgraph, useShape, ORM, SHEX, shape, store, private_store, "@graph", NURI, sparql, sparql_update, seed, wallet, RepoNotFound, FestipodData, ngGraph, bootstrap]
paths: ["src/shared/shapes/**", "src/shared/hooks/useShape*", "src/shared/context/NextGraphContext.tsx", "src/shared/context/FestipodDataContext.tsx", "src/shared/utils/ng*", "src/shared/data/seedData.ts"]
---
# Data layer
Comment Festipod **persiste ses données aujourd'hui** via NextGraph (P2P, local-first, chiffré). État actuel : **mono-store** — tout atterrit dans le `private_store` de l'utilisateur connecté.
> Distinction importante : ce concept décrit le **code actuel**. Le modèle *cible* (multi-store, multi-user, autorisations) est de la doctrine **prospective** qui vit dans le concept `nextgraph-platform` (briefs). NextGraph comme **système externe** (stores, permissions, inbox, SDK) y est aussi documenté.
**À lire avant de toucher aux écritures :** les 3 règles ci-dessous — chacune corrige un bug réel (`RepoNotFound`, suppression non persistée, redirect intempestif).
## Règles d'écriture (chacune adossée à une décision)
- [[rule_private-store-scope]] ← [[decision_2026-03-17_private-store-nuri-scope]]
- [[rule_conditional-ng-init]] ← [[decision_2026-03-13_conditional-ng-init-broker-detection]]
## Pièges (lire avant de toucher au contexte / aux suppressions / aux champs d'event)
- [[knowledge_context-internals]] — currentUser `@mariedupont`, auto-seed dev, `participantCount` cache, IRI vide, no-op local
- [[caveat_participation-deletion]] — `leaveEvent` via `ngSet.delete()` (décision SPARQL annulée), persistance possiblement partielle
- [[caveat_event-fields-not-persisted]] — `startTime`/`themes`… perdus en connecté (SHEX incomplet)
## Modèle & données
- [[knowledge_nextgraph-stack]] — paquets `@ng-org/*`, SHEX, ORM, `build:orm`
- [[knowledge_data-modes]] — connected vs disconnected/demo, providers selon le statut NG
- [[knowledge_entities]] — types `Fp*` et shapes
- [[knowledge_seed-data]] — données de seed, `CURRENT_USER_ID`
> Sécurité/confidentialité (mono-store, confiance broker) : concept `app-security`.
@@ -0,0 +1,88 @@
---
type: contract
summary: The Refine data, auth and live providers @activitypods/refine-providers exposes to an application over a Solid Pod — what each operation does, what a record looks like, and what the package does not offer
built_from: https://github.com/activitypods/refine-providers.git/src
built_version: 66616484b96fb2227cd8cdf02321aacfb4811365
built_at: 2026-10-02
---
# contract_activitypods-providers — `@activitypods/refine-providers`
**This is a RECONSTRUCTION, not the provider's promise.** The package publishes no engagement and no change policy, so this document is an observation made once, at adoption time, and pinned to the commit named in `built_version`. Nothing here is committed to by its authors: any release may move any of it. Re-observe at each upgrade — the pin is what makes a difference visible.
## Scope
Three Refine providers over a Solid Pod running ActivityPods: a `dataProvider` (CRUD over LDP containers), an `authProvider` (Solid-OIDC sign-in plus the app-registration dance ActivityPods requires), and a `liveProvider` (push over the Solid Notifications Protocol). Resources are not addressed by a path the caller chooses: each configured resource is mapped at runtime to an LDP container discovered in a WebID's Solid TypeIndex.
Two Ant Design components also ship (`antd-auth-page`, `antd-background-checks`) and are outside this scope — `antd` and `@ant-design/icons` are optional peer dependencies, so an app with its own interface consumes the three providers without them. `react-router` is likewise an optional peer dependency.
Package `@activitypods/refine-providers`, version 0.1.5 at the pinned commit. Peer dependency `@refinedev/core ^5.0.12`. Full typed shape: the package's shipped declarations, `./types` (`dist/types.d.ts`) and `./index` (`dist/index.d.ts`).
## Surface
```ts
dataProvider({ resources, authProvider, jsonContext? }): DataProvider
authProvider({ clientId, redirectUri?, allowAnonymous?, appStatusCheckInterval? }): AuthProvider
liveProvider({ resources, authProvider, jsonContext? }): LiveProvider
type ResourceConfig = { shapeTreeUri?: string; types?: string | string[] }
type ResourcesConfig = Record<string, ResourceConfig> // keyed by Refine resource name
type JsonContext = string | string[] | Record<string, any>
type AuthSession = { token: string; webId: string }
```
`AuthProvider` is Refine's `AuthProvider` plus four members: `getSession(): AuthSession | undefined` (synchronous), `handleCallback(): Promise<{ webId, redirect }>`, `registerApp(webId?): Promise<string | undefined>`, `getAppStatus(): Promise<AppStatus>`. Its `login`/`register` take `{ issuer, redirect?, isSignup? }` — the Pod provider's base URL is a caller-supplied parameter, so the app must let the user pick one.
`DataProvider` implements `getList`, `getOne`, `getMany`, `create`, `update`, `deleteOne` and `getApiUrl`. Nothing else.
Utility functions are exported from the package root as well (notably the TypeIndex discovery and container resolution used internally); they are not part of what this document engages.
## Guarantees
Observed at the pinned commit.
**A record is a JSON-LD resource with an `id`.** `id` is the resource's own URI — `id` or `@id` of the fetched document, whichever is present; a resource carrying neither raises. The record also carries `@context` (the container's, on a listed record) and every other predicate verbatim. There is exactly ONE identifier: `getOne`, `update` and `deleteOne` take that same `id` and use it directly as the URL they fetch.
**`getList`** resolves the resource's container, fetches it whole, and maps `ldp:contains`. Refine's filters and sorters are then applied **in memory**, and pagination slices the result; `total` is the count before slicing. The filter set is broad (equality, `in`, substring, prefix/suffix, ordering, `between`, null checks, and `and`/`or` nesting), string comparison being case-insensitive unless the case-sensitive variant is used, and a filter field may be a dotted path.
**`create`** POSTs `{ "@context": jsonContext, "@type": <resolved types>, ...variables }` to the container, requires a `Location` response header, and returns the record re-fetched from it. The `@type` is the resource's configured `types` expanded through `jsonContext`, or, when only `shapeTreeUri` is configured, the `sh:targetClass` of the shape the shape tree points at.
**`update` merges — a partial write is safe.** It fetches the current resource, drops its `id` and `@context`, and PUTs `{ "@context": jsonContext, ...current, ...variables }`. So values the caller omits survive. The merge is **shallow**: a nested object in `variables` replaces its counterpart whole. It returns the record re-fetched after the write.
**`deleteOne`** DELETEs the resource and returns `{ id }` only.
**A configured resource's container is discovered, not configured.** Resolution reads the WebID document, then its `solid:publicTypeIndex`, then best-effort its `pim:preferencesFile` → `solid:privateTypeIndex`, and takes the first type registration whose `solid:forClass` matches the resource's resolved types and which carries a `solid:instanceContainer`. No match raises, naming the WebID and the types. A `shapeTreeUri` is fetched over plain HTTP, as is the shape it points at — so a shape tree the caller hosts itself works, provided it is reachable and carries `st:shape` → `sh:targetClass`.
**`jsonContext`** is sent as `@context` on every create and update, and is what expands configured CURIE types. It defaults to the ActivityStreams 2 context; an application with its own vocabulary passes its own.
**`liveProvider` pushes — the caller never polls.** It opens a `notify:WebSocketChannel2023` channel per topic, discovering the subscription endpoint from the Pod origin's `.well-known/solid`. Watching specific records subscribes to each record and reports `Update` → `updated`, `Delete`/`Remove` → `deleted`. Watching a list subscribes to the **container** and reports `Add` → `created`, `Remove` → `deleted`. Unsubscribing closes the socket and deletes the channel best-effort. A resource absent from `resources` yields a no-op unsubscribe rather than an error.
**Auth is Solid-OIDC with PKCE.** `login` redirects to the issuer's authorization endpoint with scope `openid webid offline_access`; `handleCallback`, called from the redirect route, completes the code grant and stores the ID token. The session's `webId` is the token's `webid` claim, and an expired token reads as no session. `logout` clears local state. `check` reports unauthenticated with `redirectTo: "/login"` unless `allowAnonymous` is set. `getIdentity` returns `{ id: webId, name, avatar }`, name and avatar read from the WebID document and the profile it links — ActivityPods' own profile vocabulary, unrelated to any vocabulary the app defines for its own user records. `registerApp` returns the AppRegistration URI when the app is registered and its grants current, and otherwise redirects the browser to the user's authorization agent and resolves `undefined`.
**Failures throw, and carry the HTTP status.** Every Pod call that returns non-2xx raises an `Error` with `status` and `body` attached, its message taken from the response body's error message when there is one. `onError` maps a 401 to logout.
## Non-guarantees
**Every read and write is scoped to the logged-in user's own Pod.** Container resolution always runs against the session's own WebID — in `getList`, in `create`, and in a list subscription alike. There is no parameter, filter or option through which an operation reaches another user's resources: `getList` returns what the current user holds, and nothing else. (`getOne`/`getMany` fetch a URI directly and so are not bound this way, but they require the caller to already hold that URI.)
**No server-side querying.** Filtering, sorting and pagination are applied after the whole container has been fetched, so cost grows with the container, not with the page.
**`getManyReference` is not implemented**, despite the data provider's own documentation mentioning it. Neither are `createMany`, `updateMany` and `deleteMany`.
**`getMany` drops what it cannot fetch.** Failed fetches are filtered out silently, so the returned array may be shorter than `ids` with no indication of which are missing.
**Caches are module-level and never invalidated** — shape trees by URI, type registrations by WebID, subscription endpoints by origin. A container that becomes available after a first lookup (the user granting access, for instance) is not seen until the page reloads, which is what the registration flow expects.
**`authProvider` installs process-global timers it never removes.** Unless `appStatusCheckInterval: false` is passed, construction registers a `setInterval` and a `visibilitychange` listener, with no teardown; constructing the provider more than once per page accumulates them.
**One session per page.** The token lives at a fixed `localStorage` key, so two concurrent sessions cannot coexist.
**A list subscription does not report field changes.** A container notifies about membership only, so a member's own predicates changing is invisible to a list; the record has to be watched directly.
**`types` wins over `shapeTreeUri` when both are set** — the opposite of what the configuration type's own documentation states. Configure exactly one. Likewise, when several type registrations match a resource's types, the first encountered wins, with no stated ordering.
**Unregistered apps read and write nothing.** An ActivityPods app must hold an AppRegistration before any data operation succeeds, and keeping it current is the app's business, through `registerApp` and `getAppStatus`.
## Change policy
None is published. The package carries no engagement document, no stated versioning commitment and, at the pinned commit, no git tags — its released versions are visible only as `package.json` version bumps. Treat every upgrade as potentially breaking: re-read the surface against the new commit and re-stamp this document. Divergences found between this reconstruction and observed behaviour are defects in this document, to be fixed here.
@@ -0,0 +1,71 @@
---
type: usage
summary: What Festipod consumes from @activitypods/refine-providers as one of its interchangeable Refine backends, and the conditions its own vocabulary and confidentiality model put on that backend
against: 66616484b96fb2227cd8cdf02321aacfb4811365
---
# usage_festipod — Festipod on `@activitypods/refine-providers`
Festipod is a mobile-first web application: users create **meeting points** grafted onto public events, and sign up to them. Its entire persistence goes through Refine, behind a **backend seam** that exists so the app can support several Refine backends and let the user switch between them at runtime. This package is one of the two backends targeted; the other is a Refine provider over NextGraph plus its polyfill. The seam's own contract — what a backend must answer before it can be added — lives in code, in `src/shared/refine/providers/types.ts`.
Nothing is wired yet, so the list below is **declared, not measured**: it is what Festipod will call, derived from the seam's requirements and from the observation of the package pinned beside this declaration. `against:` names that observation's commit. The package publishes no engagement, so there is nothing to pull and nothing that can drift — what is recorded here is read against our own reconstruction, and re-read when it is re-stamped.
## Consumed surface
### Provider construction
- `authProvider({ clientId, redirectUri, appStatusCheckInterval: false })` — one call site, per backend selection. `appStatusCheckInterval` is disabled deliberately, see friction 1; Festipod drives the check itself.
- `dataProvider({ authProvider, resources, jsonContext })` — one call site, handed the `authProvider` above.
- `liveProvider({ authProvider, resources, jsonContext })` — same, so Refine can subscribe behind every query.
All three are built together, **per backend instance**, inside the seam's `build()`.
### Auth members
`login({ issuer, redirect })` and `register(...)` — Festipod supplies its own Pod-provider picker. `handleCallback()` from its own callback route. `logout()`. `check()`. `getSession()` for the `webId` and the token. `registerApp()` and `getAppStatus()`, the second one on Festipod's own schedule since the built-in interval is off.
### Resources and operations
Four resources, one per persisted entity: **events**, **meeting points**, **participations** and **user profiles**. Each is configured with `types` naming Festipod's own class URIs, and `jsonContext` carries the context that expands them.
Through Refine's hooks only, never the provider directly: `useList` → `getList`, `useCreate` → `create`, `useUpdate` → `update`, `useDelete` → `deleteOne`. `getOne` is reached by Refine's cache plumbing rather than an explicit call. The app mounts Refine with `liveMode: 'auto'` and `mutationMode: 'optimistic'`.
### Record handling
A record's `id` — the resource's URI — is the **only** identifier Festipod uses: the seam's `recordKey` and `domainId` both return it. `update` merging the fields it is given means the seam declares this backend as patching, and sends only what changed.
### Not consumed
- `shapeTreeUri` — exactly one of the two resource identifiers must be configured, and `types` names Festipod's classes directly, without a shape tree to host and keep reachable.
- `getIdentity` — the signed-in identity and a Festipod user profile are two unrelated things in this app; the profile is read as an ordinary record, and only `getSession`'s `webId` is needed.
- `getMany`, `getApiUrl`, `allowAnonymous`, and both Ant Design entry points — Festipod has its own interface.
## Constraints
What must keep holding for this backend to serve the app.
1. **Festipod's own vocabulary, not ActivityStreams.** The four entities are Festipod domain types defined by SHEX shapes this repo holds; none is an ActivityStreams type. A caller-supplied `jsonContext` and caller-supplied `types` are what make that possible, and both must stay caller-supplied.
2. **The identifier a record is addressed by must stay the identifier other records reference.** The seam carries the two separately because they differ on the other targeted backend; here they coincide, and the app relies on that. Splitting them later would break every cross-entity reference silently.
3. **A partial update must stay partial.** The app sends only changed fields. A move to replacing the resource would drop every field omitted, with no error.
4. **Every participation must be readable by every user.** The participant count is the number of participations of an event, counted with no filtering. What is confidential is the **link between a participation and a person** — only a connection can read the profile a participation points at and recognise who it is — never the participation record itself. See need 1: this is the constraint the package does not currently let the app satisfy.
5. **The count is never a stored, incremented field.** Reading a counter, adding one and writing it back derives each write from a read of the previous one and races itself through any data provider. This is measured, not theoretical, and it is why the app derives the count from the participation list at read time.
6. **Changes must be pushed, not polled.** Neither the app nor its BDD suites has a polling loop, and adding one is ruled out by project doctrine. `liveMode: 'auto'` is mounted on that basis.
7. **No process-global state that outlives a remount.** Two backends coexist in one page and the user switches between them at runtime; switching remounts Refine to discard the cache of the backend being left. Building this backend's providers must have no side effect that accumulates across switches — see friction 1, which is where this currently bites.
8. **Each entity carries a declared visibility** — public, protected or private, per the product's confidentiality model — and it is the backend that honours it. Refine has no notion of it, so the intent is declared at the seam and enforcement is the backend's.
## Frictions
1. **`authProvider` installs a timer and a listener it never removes.** Construction registers a `setInterval` and a `visibilitychange` listener with no teardown. Festipod builds providers once per backend selection, so every switch to this backend would accumulate another pair — directly against constraint 7. Workaround carried: pass `appStatusCheckInterval: false` and call `getAppStatus()` on the app's own schedule. That works, but it means the package's own staleness handling is unusable here, and the only way to opt out is to opt out entirely.
2. **`getList` fetches the whole container on every call.** Filtering, sorting and pagination run in memory afterwards, so the cost of listing an event's participations grows with the user's total number of participations rather than with the event. No workaround; it is why a filter on a `getList` buys nothing beyond convenience.
3. **A list subscription reports membership only.** A container channel delivers `Add`/`Remove`, not a member's own fields changing, so a list of events does not refresh when an event's title, place or time is edited. Watching each record instead would open one channel per row. The event list is exactly the screen where this is visible.
4. **`types` takes precedence over `shapeTreeUri`, the opposite of what the configuration type documents.** The declared behaviour is that `types` is "only used as a fallback when `shapeTreeUri` is not set"; a resource setting both is in fact resolved from `types`. Establishing which one actually wins cost a read of the resolution path. Configuring exactly one avoids it, but the documentation should say which wins.
## Needs
1. **A read that is not scoped to the logged-in user's own Pod.** Every container resolution — in `getList`, in `create`, and in a list subscription — runs against the session's own WebID. So `getList('events')` returns only the events the current user created, and `getList('participations')` only their own participations.
This blocks the product, not a detail of it. Festipod exists to let a user **discover other people's public events** and see **how many people have signed up** to a meeting point. Through this provider the app can show neither: no other user's event is reachable, and the participant count of constraint 4 cannot be computed from participations it cannot read.
The request is modest in shape, because the capability already sits one layer down: the package's own TypeIndex discovery takes a WebID as a parameter, and notes that the public TypeIndex is readable without any special grant. What is missing is a way to reach it **through the data provider** — a way to scope a `getList` (and the matching list subscription) to a given WebID, or to a set of them. Festipod has no interest in reimplementing container resolution beside the provider; that would put the app back in the business the provider exists to hold.
2. **Releases that can be pinned, and a statement of what changes between them.** The package carries no git tags, so the only thing this repo can pin is a commit on a moving branch, and the only way to learn what an upgrade changed is to re-read the source. Tagged releases plus a note of what moved in each — in whatever form suits the project — would let a consumer upgrade deliberately instead of re-observing from scratch every time.
@@ -0,0 +1,17 @@
---
type: caveat
summary: Le type FpEventData et le seed portent startDate/endDate/startTime/endTime/themes, mais le SHEX Event ne les définit pas — ces champs sont silencieusement perdus en mode connected (NextGraph)
last_checked: 2026-06-15
---
# Caveat : champs d'événement non persistés en mode connected
Le type app `FpEventData` (`src/shared/data/types.ts`) et le seed (`seedData.ts`) portent des champs **`startDate`, `endDate`, `startTime`, `endTime`, `themes`** — mais la **shape SHEX `Event`** (`src/shared/shapes/shex/festipodShapes.shex`) ne les définit **pas**. La shape ne couvre que : `title, description, date, location, distance, participantCount, coverImage, hostName, hostInitials` (à vérifier dans le `.shex`).
## Conséquence
En **mode connected** (NextGraph), le mapping (`mapEvent` dans `FestipodDataContext.tsx`) ne lit/écrit que les champs de la shape. Les champs hors-shape sont **silencieusement perdus** : remplis par des defaults ou vides. Or des écrans **les affichent** (ex. `startTime`/`endTime` dans `EventDetailScreen`) — donc en mode démo (seed local) ils apparaissent, mais en connecté ils disparaissent. Décalage observable seulement à l'usage.
## Pour corriger (si on veut les persister)
Ajouter les champs à `festipodShapes.shex` puis `bun run build:orm`, et étendre `mapEvent`. C'est aussi un prérequis de la modélisation complète du point de rencontre (cf. concept `nextgraph-platform`, [[brief_2026-05-21_fork-nextgraph-inbox]] §Couche 3). Tant que ce n'est pas fait, **ne pas se fier aux champs date/heure/thèmes en mode connecté**.
@@ -0,0 +1,23 @@
---
type: caveat
summary: La suppression de Participation (leaveEvent) se fait via ngSet.delete() — le bug de non-persistance qui avait motivé SPARQL DELETE est en grande partie corrigé, mais la persistance peut rester partielle ; vérifier après refresh
last_checked: 2026-06-15
---
# Caveat : suppression de Participation via `ngSet.delete()`
**État actuel du code** (`src/shared/context/FestipodDataContext.tsx`, `leaveEvent` en mode NG) : la suppression d'une `Participation` se fait via **`participationsShape.ngSet.delete(ngPart)`** — pas via `ng.sparql_update()` DELETE WHERE.
## Histoire (important)
Une décision antérieure ([[decision_2026-03-17_sparql-delete-for-orm-objects]], **annulée le 2026-06-15**) imposait SPARQL DELETE car `ngSet.delete()` ne persistait pas (l'objet réapparaissait au refresh). Ce **bug du `@ng-org/orm` a depuis été en grande partie corrigé** : `ngSet.delete()` est redevenu le chemin utilisé.
## Le piège (pourquoi un caveat et pas une règle)
La correction **semble partielle** : selon les cas, la suppression via `ngSet.delete()` peut ne **pas se propager complètement** au broker. Donc :
- **Ne pas tenir pour acquis** que `leaveEvent` persiste à coup sûr — **vérifier après un vrai refresh** que la participation a bien disparu côté wallet.
- Si une suppression se révèle non persistée, le repli connu reste `ng.sparql_update()` avec `DELETE WHERE { GRAPH <…> { <…> ?p ?o } }` (le mécanisme décrit dans la décision annulée). **Ne pas combiner** les deux (conflit CRDT — c'était l'autre enseignement de la décision).
- Re-tester ce point à chaque montée de version de `@ng-org/orm`.
> À valider : ouvrir `FestipodDataContext.tsx` → `leaveEvent` (mode NG, `console.log('Deleting participation via ngSet.delete()')`). Si le code est repassé à `sparql_update`, mettre ce caveat à jour ou le promouvoir en règle.
@@ -0,0 +1,35 @@
---
type: decision
summary: Décision 2026-03-13 — auto-init NextGraph seulement quand dans l'iframe broker (window.self !== window.top), sinon initNgWeb() redirige la page et casse le dev/démo standalone
---
# Conditional NextGraph Init Based on Broker Iframe Detection
**Date:** 2026-03-13 14:00
**Status:** Accepted
## Context
`initNgWeb()` de `@ng-org/web` teste `window.self === window.top`. En standalone (hors iframe), il redirige toute la page vers `nextgraph.net/redir/` pour déclencher l'auth broker. Résultat : l'app redirigeait à chaque chargement — même en dev ou quand l'utilisateur n'avait pas cliqué « Se connecter ».
## Options Considered
### Option A: toujours auto-init NG au mount
- Plus simple (pas de branchement).
- **Contre** : redirect immédiat vers le broker en standalone ; casse le workflow de dev ; l'utilisateur voit la page de login broker au lieu de l'app.
### Option B: auto-init conditionnel selon détection iframe
- En iframe, le broker a déjà authentifié → auto-init sûr ; en standalone, l'utilisateur doit cliquer « Se connecter » ; préserve l'expérience démo/dev ; calque la propre logique de détection de `@ng-org/web`.
- **Contre** : repose sur l'heuristique `window.self !== window.top` (théoriquement faillible si embarqué dans une iframe non-broker).
## Decision
**Option B.** `NextGraphContext` calcule `isInsideBroker = typeof window !== 'undefined' && window.self !== window.top` au niveau module. `useEffect` n'auto-appelle `initNg()` que si `isInsideBroker`. Le callback `connect()` reste disponible pour la connexion explicite. De plus, `FestipodDataContext` rend des données vides (pas le seed) pendant `connecting` pour éviter de flasher le contenu démo.
## Consequences
**Positif :** l'app charge sans rediriger (standalone dev/démo) ; en iframe broker, connexion fluide et automatique ; pas de flash de seed pendant la connexion.
**Négatif :** aucun significatif.
**Risque :** si `@ng-org/web` change sa logique de détection, notre garde peut diverger — les garder alignés.
> Règle dérivée : [[rule_conditional-ng-init]].
@@ -0,0 +1,39 @@
---
type: decision
summary: Décision 2026-03-17 — utiliser private_store_id comme scope useShape ET @graph (calqué sur expense-tracker-rdf) pour que orm_start_graph ouvre le repo et que les écritures ne lèvent plus RepoNotFound
---
# Use private_store_id as useShape scope and @graph
**Date:** 2026-03-17 16:00
**Status:** Accepted
## Context
Cliquer « Charger données de test » chargeait les données en mémoire (signaux ORM) mais produisait des `RepoNotFound` sur `doc_create` et `orm_frontend_update`. Les données disparaissaient au reload car les écritures SPARQL n'atteignaient jamais le broker. La HashMap `self.repos` du verifier ne contenait pas le repo du private store → `resolve_target()` échouait.
## Options Considered
### Option A: `did:ng:i` scope + `doc_create` pour @graph
- `did:ng:i` bien documenté comme scope d'abonnement, `doc_create` renvoie un vrai NURI.
- **Contre** : `did:ng:i` passe par `NuriTargetV0::UserSite` qui n'ouvre pas les repos individuels ; `doc_create` appelle `resolve_target(PrivateStore)` qui exige le repo dans `self.repos` → échoue ; exige une logique de retry/timing complexe.
### Option B: `private_store_id` comme scope ET @graph
- Calque exact de l'exemple `expense-tracker-rdf` qui fonctionne ; `orm_start_graph` avec le NURI du private store ouvre le repo dans `self.repos` ; les écritures `orm_frontend_update` trouvent ensuite le repo. Simple, sans retry.
- **Contre** : un peu moins flexible que `did:ng:i` (scopé à un store) ; exige de passer la session à `useShapeWithDefaults`.
### Option C: `did:ng:i` scope + réutiliser le @graph d'une entité existante
- Marche pour les users qui ont déjà des données.
- **Contre** : échoue pour les wallets vides (aucune entité à réutiliser) ; retombe sur `doc_create` et le même `RepoNotFound`.
## Decision
**Option B** : `did:ng:${session.private_store_id}` comme scope `useShape` ET `@graph` d'écriture, exactement comme `expense-tracker-rdf`. `useShapeWithDefaults` accepte un `storeNuri` ; `FestipodDataContext.useNgData()` récupère la session via `useNextGraph()` et passe le NURI du private store. `ensureGraphNuri()` simplifié : entités existantes d'abord (optimisation), sinon fallback `private_store`.
## Consequences
**Positif :** écritures immédiates après connexion (sans retry) ; persistance au reload ; aligné sur les exemples officiels ; les 7 scénarios e2e passent (dont la persistance).
**Négatif :** signature de `useShapeWithDefaults` modifiée (param `storeNuri`).
**Risque :** si NextGraph change le comportement du private store, ça casse.
> Règle dérivée : [[rule_private-store-scope]]. Décision *remise en cause* par le futur multi-store : [[brief_2026-05-17_multi-store-refactor]].
@@ -0,0 +1,51 @@
---
type: decision
summary: Décision 2026-03-17 — supprimer les objets ORM via ng.sparql_update (DELETE WHERE) seul, car ngSet.delete() ne persiste pas et les combiner crée un conflit CRDT
---
# Use SPARQL DELETE instead of ORM ngSet.delete() for object removal
**Date:** 2026-03-17 18:00
**Status:** ~~Accepted~~ → **Superseded (2026-06-15)**
> **Annulée le 2026-06-15.** Le bug de non-persistance de `ngSet.delete()` qui motivait cette décision a depuis été en grande partie corrigé côté `@ng-org/orm` : le code (`leaveEvent`) est repassé à `ngSet.delete()`. La persistance reste toutefois possiblement partielle — l'état courant et le repli SPARQL sont décrits dans [[caveat_participation-deletion]]. Décision conservée comme mémoire d'arbitrage (le conflit CRDT « ne pas combiner les deux » reste vrai).
## Context
Quitter un event exige de supprimer l'objet `Participation` du store NextGraph. `DeepSignalSet.delete()` met à jour l'état réactif local (UI immédiate) mais **ne persiste pas** au broker — après refresh, la participation réapparaît.
## Options Considered
### Option A: ORM `ngSet.delete(item)`
- API officielle (README ORM), update réactif local instantané.
- **Contre** : ne persiste pas en pratique (`delete()` renvoie `true`, set local à jour, mais objet de retour après refresh) ; `graph_orm_update` semble mal gérer les patches "remove" pour objets de set top-level (bug moteur probable) ; échoue silencieusement.
### Option B: `ng.sparql_update()` avec SPARQL DELETE
- `DELETE WHERE { GRAPH <graph> { <subject> ?p ?o } }` retire tous les triples RDF.
- **Pour** : persiste (survit au refresh) ; le broker confirme via `GraphOrmUpdate` remove qui retire réactivement l'item du set ORM ; contrôle direct.
- **Contre** : pas instantané (round-trip SPARQL + callback broker, ~50ms) ; ne doit pas être combiné avec `ngSet.delete()`.
### Option C: les deux ensemble
- **Ne marche pas** : le patch ORM `.delete()` et le DELETE SPARQL entrent en conflit CRDT → ni UI ni persistance.
## Decision
**Option B : SPARQL DELETE seul.** Le broker renvoie un `GraphOrmUpdate` `op: "remove"` qui retire réactivement l'item du set ORM (UI à jour, juste pas synchrone). **Ne pas** appeler `ngSet.delete()` à côté.
```typescript
// FestipodDataContext.tsx leaveEvent():
const session = await sessionPromise;
await ng.sparql_update(
session.session_id,
`DELETE WHERE { GRAPH <${partGraph}> { <${partId}> ?p ?o } }`,
partGraph,
);
```
## Consequences
**Positif :** suppression persistée ; source de vérité unique (broker → ORM → UI).
**Négatif :** léger délai UI (~50ms) ; diverge des exemples README ORM.
**Risque :** si `ng.sparql_update` change, ça casse ; toute future suppression doit suivre le même pattern ; revisiter si `ngSet.delete()` est corrigé en montée de version.
> État courant (la règle a été retirée) : [[caveat_participation-deletion]].
@@ -0,0 +1,30 @@
---
type: knowledge
summary: Pièges internes de FestipodDataContext — currentUser NG résolu par username '@mariedupont' (fallback users[0]), auto-seed dev-only après 3s sans retry, participantCount muté en place (cache), currentUserId vide → IRI invalide, mutations no-op en mode local malgré le toast
last_checked: 2026-06-15
---
# Internals & pièges de `FestipodDataContext`
Comportements non évidents de `src/shared/context/FestipodDataContext.tsx` à connaître avant de toucher au contexte de données.
## Résolution du `currentUser` (mode NG)
En mode connected, le currentUser n'est **pas** `CURRENT_USER_ID` ('user-1', qui ne vaut qu'en mode local). Il est résolu par **`users.find(u => u.username === '@mariedupont') || users[0]`** (vers ligne 286). Pièges :
- **Fallback silencieux** sur `users[0]` si `@mariedupont` absent → currentUser arbitraire.
- Si le wallet est **vide** (`users.length === 0`), `currentUserId` devient `''` → toute `Participation` créée a un `user: ''` (**IRI invalide**), sans alerte. Bug silencieux possible à la première connexion sur un wallet vierge.
- L'IRI du currentUser diffère entre mode local (ID de seed statique) et mode NG (IRI NextGraph dynamique) — ne pas comparer les deux.
## Auto-seed de dev
Un auto-seed se déclenche (vers lignes 263-283) **uniquement hors production** (`process.env.NODE_ENV !== 'production'`), après un **`setTimeout` de ~3s**, si les sets events ET users sont vides. Pièges :
- **Pas de retry** : `hasTriedAutoSeed` (useRef) est posé une fois ; si le seed échoue, jamais réessayé (écran vide, juste un `console.error`).
- Le délai de 3s est **heuristique** : si l'hydratation ORM est lente, le seed peut partir alors que des données arrivent.
## `participantCount` muté en place
`joinEvent`/`leaveEvent`/`updateEvent` **mutent directement** `ngEvent.participantCount` (`+1`/`-1`) — c'est un **cache** du nombre de `Participation`, pas une valeur recalculée. Il peut **désynchroniser** des objets `Participation` réels (ex. après un crash, un rejeu, ou la suppression partielle décrite dans [[caveat_participation-deletion]]). Ne pas s'y fier comme source de vérité du nombre de participants.
## Mutations no-op en mode local
En mode local/demo (`useLocalData`), `createEvent`/`joinEvent`/`leaveEvent`/`updateEvent` sont des **no-ops** (`console.log`, l'état ne change pas) — mais les écrans affichent quand même un **toast de succès** (« Tu participes »). UX potentiellement trompeuse : l'utilisateur croit s'être inscrit alors que rien n'a changé. Voir [[knowledge_data-modes]] pour le choix du provider selon le statut.
@@ -0,0 +1,29 @@
---
type: knowledge
summary: Deux modes (connected = NextGraph ORM, disconnected/demo = état local seedé) ; FestipodDataContext choisit le provider selon le statut NextGraphContext, tous les écrans passent par useFestipodData()
---
# Modes de données & contextes
L'app a **deux modes**, tous deux consommés via le hook `useFestipodData()` :
1. **Connected** — shapes ORM NextGraph (P2P, chiffré, local-first)
2. **Disconnected / Demo** — état React local seedé depuis `seedData.ts` (voir [[knowledge_seed-data]])
## NextGraphContext (`src/shared/context/NextGraphContext.tsx`)
- Cycle de connexion : `disconnected` → `connecting` → `connected` | `error`.
- Fournit la session avec les IDs de stores (private, protected, public).
- **Auto-init conditionnel** : voir [[rule_conditional-ng-init]] (n'auto-initialise que dans l'iframe broker).
## FestipodDataContext (`src/shared/context/FestipodDataContext.tsx`)
- Enveloppe les shapes via `useShapeWithDefaults()`.
- Expose `useFestipodData()` (consommé par tous les écrans) + CRUD (`createEvent`, `updateEvent`, etc.).
- **Provider selon le statut NG** :
- `disconnected` → `LocalDataProvider` avec seed (démo)
- `connecting` → `LocalDataProvider` **vide** (évite de flasher le seed avant le chargement du wallet)
- `connected` → `NgDataProvider` (données réelles du wallet)
- `error` → `LocalDataProvider` avec seed (fallback gracieux)
> Réserve : certaines mutations (`joinEvent`/`leaveEvent`) sont encore des **no-ops** (`console.log`) en attendant le chantier données — cf. [[brief_2026-05-21_fork-nextgraph-inbox]] §Couche 3.
@@ -0,0 +1,20 @@
---
type: knowledge
summary: Types de données Fp* (Event, UserProfile, Participation persistés NextGraph ; MeetingPoint et Friendship encore local-only)
---
# Entités de données
`src/shared/data/types.ts` :
| Type | Persistance | Champs clés |
|---|---|---|
| `FpEventData` | NextGraph (shape Event) | id, title, date, location, distance, themes |
| `FpUserData` | NextGraph (shape UserProfile) | id, name, username, bio, city, counts |
| `FpParticipationData` | NextGraph (shape Participation) | eventId + userId + confirmed |
| `FpMeetingPointData` | **local-only** | eventId, location, time, host |
| `FpFriendshipData` | **local-only** | userId + friendId |
`MeetingPoint` et `Friendship` n'ont **pas encore de shape SHEX** ni de persistance NextGraph (cf. [[knowledge_nextgraph-stack]]). Les brancher au store est un prérequis du multi-user — voir les briefs du concept `nextgraph-platform`.
> Piège : même pour `FpEvent` (persisté), plusieurs champs du type app ne sont **pas** dans la shape et sont perdus en connecté — voir [[caveat_event-fields-not-persisted]].
@@ -0,0 +1,26 @@
---
type: knowledge
summary: Paquets @ng-org/* (web, orm, shex-orm, alien-deepsignals), shapes SHEX festipodShapes, bindings ORM générés, régénérés via build:orm
---
# Stack NextGraph (côté app)
```
@ng-org/web # Runtime navigateur (proxy postMessage vers l'iframe)
@ng-org/orm # ORM réactif basé sur les shapes RDF (useShape…)
@ng-org/shex-orm # Génération SHEX → TypeScript
@ng-org/alien-deepsignals # Pont de signaux réactifs
```
Installés depuis npm (`@ng-org/*`, versions alpha). Pour développer contre un build local non publié de `nextgraph-rs`, `scripts/build-ng-packages.sh` pack le monorepo en tarballs et repointe `package.json` (cf. `nextgraph-platform` — le pattern d'origine du projet, réactivable pour un fork).
## Shapes SHEX
`src/shared/shapes/shex/festipodShapes.shex` définit :
- **Event** — titre, description, dates, lieu, thèmes, participants
- **UserProfile** — nom, username, bio, ville, visibilité
- **Participation** — lie event + user, statut de confirmation
Bindings ORM dans `src/shared/shapes/orm/` (`*.schema.ts`, `*.shapeTypes.ts`, `*.typings.ts`). **Régénérer** avec `bun run build:orm` après toute modif `.shex`.
> Manque côté shapes : **pas de `MeetingPoint`** ni d'entité notification — le point de rencontre est aujourd'hui local-only côté types (voir [[knowledge_entities]]). Leur modélisation est un chantier de [[brief_2026-05-21_fork-nextgraph-inbox]].
@@ -0,0 +1,17 @@
---
type: knowledge
summary: seedData.ts fournit des fixtures déterministes (10 users, events, participations) avec CURRENT_USER_ID = 'user-1' (Marie Dupont) ; utilisé en mode démo et par les tests @ui
---
# Seed data
`src/shared/data/seedData.ts` fournit des fixtures **déterministes** :
- 10 users — **Marie Dupont = utilisateur courant**, `user-1`
- Plusieurs events (dates, lieux, thèmes)
- Participations, meeting points, friendships
- `CURRENT_USER_ID = 'user-1'`
Ces fixtures servent (a) le **mode démo** (`LocalDataProvider`, cf. [[knowledge_data-modes]]) et (b) les tests **`@ui`** qui rendent les écrans avec ces données prévisibles (`Marie Dupont`/`@mariedupont` = currentUser, `Jean Durand`/`@jeandurand` existe, etc. — voir concept `bdd-testing`).
> `bootstrapWallet()` (`src/shared/utils/ngBootstrap.ts`) seede ces données dans le wallet NG en mode connected — déclenché uniquement par action explicite de l'utilisateur (« Charger données de test »). Sa refonte par documents/périmètres est un point des briefs `nextgraph-platform`.
@@ -0,0 +1,14 @@
---
type: rule
summary: N'auto-initialiser NextGraph que dans l'iframe broker (window.self !== window.top) ; en standalone, initNgWeb() redirige toute la page — attendre un connect() explicite
---
# Règle : auto-init NextGraph seulement dans l'iframe broker
`initNgWeb()` de `@ng-org/web` teste `window.self === window.top`. **Hors iframe** (app standalone), il **redirige toute la page** vers `nextgraph.net/redir/` pour déclencher l'auth broker.
Donc `NextGraphContext` calcule `isInsideBroker = window.self !== window.top` et **n'auto-appelle `initNg()` que si `isInsideBroker`**. En standalone, la connexion attend un `connect()` explicite (clic « Se connecter ») — sinon l'app redirige à chaque chargement et casse le dev/démo.
De plus, `FestipodDataContext` rend des données **vides** (pas le seed) pendant la phase `connecting`, pour éviter de flasher du contenu démo avant le chargement du wallet (voir [[knowledge_data-modes]]).
> Garder ce garde **aligné** sur la détection interne de `@ng-org/web` : si leur heuristique change, le nôtre doit suivre. Pourquoi + alternatives : [[decision_2026-03-13_conditional-ng-init-broker-detection]].
@@ -0,0 +1,25 @@
---
type: rule
summary: Utiliser did:ng:${private_store_id} comme scope useShape ET comme @graph d'écriture ; ne jamais utiliser did:ng:i comme scope (casse toutes les écritures par RepoNotFound)
---
# Règle : scope = `@graph` = private_store_id
Pour lire **et** écrire via l'ORM NextGraph :
- **Scope** : `useShape(shapeType, \`did:ng:${session.private_store_id}\`)`
- **`@graph`** (cible des écritures) : `did:ng:${session.private_store_id}`
C'est critique : `orm_start_graph` avec le NURI du private_store **ouvre explicitement le repo** dans la HashMap `self.repos` du verifier. Sans ça, `orm_frontend_update` échoue en `RepoNotFound`.
## Interdit
**Ne pas utiliser `did:ng:i` comme scope.** Il s'abonne au site entier de l'utilisateur via un chemin de code spécial (`NuriTargetV0::UserSite`) qui **n'ouvre pas les repos individuels** → casse toutes les écritures.
## Fichiers porteurs
- `src/shared/hooks/useShapeWithDefaults.ts` — accepte un `storeNuri`, le passe à `useShape`.
- `src/shared/utils/ngGraph.ts` — `ensureGraphNuri()` retourne le `@graph` (entités existantes d'abord, sinon fallback `private_store`).
- `src/shared/utils/ngBootstrap.ts` — seede en utilisant `ensureGraphNuri()`.
> Le *pourquoi* complet et les alternatives écartées : [[decision_2026-03-17_private-store-nuri-scope]]. **Ce scope mono-store est précisément ce que le chantier multi-store viendra remplacer** — voir [[brief_2026-05-17_multi-store-refactor]].
@@ -0,0 +1,28 @@
---
type: _overview
summary: Modèle produit Festipod — le point de rencontre greffé sur un événement public comme unité de valeur, ses acteurs et ses concepts métier
triggers:
keywords: [point de rencontre, rencontre, greffe, greffer, événement, déclarant, hôte, inscrit, inscription, communauté, connexion, festival, déduplication]
paths: ["src/modules/*/features/**"]
---
# Functional domain
Le **domaine fonctionnel** de Festipod : ce que le produit promet et le vocabulaire métier qui le décrit. Source d'origine : `README.md §Modèle fonctionnel`.
**À lire en premier :** [[knowledge_business-model]] — sans lui, on confond l'événement (l'ancrage) et le point de rencontre (la valeur), et on modélise à l'envers.
## Idée pivot
Festipod laisse les utilisateurs créer des **points de rencontre** qui se *greffent* sur des **événements publics** existants. L'événement (festival, conférence…) n'est qu'un *prétexte* et un point d'ancrage spatio-temporel ; la valeur produite, c'est le point de rencontre. **On s'inscrit à un point de rencontre, jamais à un événement.**
## Périmètre & sécurité
Le modèle d'**autorisations / confidentialité** (qui voit quoi : « données personnelles = réseau seulement », anonymat via inbox, capabilities) n'est pas encore implémenté — il vit aujourd'hui comme incubation dans [[brief_2026-05-18_authorization-matrix]] (concept `nextgraph-platform`). Il graduera en règles/`behavior_` quand le multi-user atterrira. C'est la raison pour laquelle il n'y a pas encore de concept `app-security` distinct.
## Liens
- [[knowledge_actors-and-concepts]] — référence des acteurs et concepts métier
- [[knowledge_roadmap]] — fonctionnalités actuelles vs évolutions à venir
- [[brief_2026-06-15_event-deduplication]] — défi ouvert de déduplication des événements en P2P
- `nextgraph-platform` — où vit la dérivation de la structure de données cible (authz matrix, multi-store)
@@ -0,0 +1,23 @@
---
type: brief
summary: Défi ouvert — en infra P2P, deux utilisateurs peuvent déclarer le même événement public et fragmenter les points de rencontre greffés ; pistes non tranchées
---
# Déduplication des événements en infra décentralisée
**Status:** Défi ouvert — non tranché
**Capturé:** 2026-06-15 (issu de `README.md §Défis ouverts`)
## Problème
NextGraph étant P2P, rien n'empêche deux utilisateurs de **déclarer indépendamment le même événement public** (par ex. « Eurockéennes 2027 ») et de produire deux entrées distinctes. La dispersion qui en résulte **fragmente les points de rencontre greffés** et réduit leur visibilité — ce qui va à l'encontre de la fonction première de l'app (cf. [[knowledge_business-model]]).
## Pistes envisagées (non tranchées)
- **Recherche avant création** — proposer à l'utilisateur, lors de la déclaration, les événements déjà déclarés dans son réseau / ses communautés qui correspondent à sa saisie.
- **Identifiant externe canonique** — utiliser une URL officielle de l'événement, Wikidata, ou `schema.org/Event` pour reconnaître les doublons et les présenter comme un seul événement à l'affichage.
- **Curation** — laisser des curators (humains ou communautaires) fusionner / vetter les entrées canoniques.
## Lien avec le modèle d'écriture
Ce défi est couplé à une question ouverte de [[brief_2026-05-18_authorization-matrix]] : **qui peut modifier un événement déclaré** (propriétaire / wiki / immuable). Un modèle *wiki* faciliterait la convergence ; un modèle *propriétaire* la complique. À arbitrer ensemble.
@@ -0,0 +1,31 @@
---
type: knowledge
summary: Référence des acteurs (utilisateur, connexion, déclarant, hôte, inscrit, membre) et des concepts métier (point de rencontre, événement, communauté, liste curated, connexion)
---
# Acteurs et concepts métier
Référence du vocabulaire. Tous les acteurs sont des spécialisations d'un **utilisateur** authentifié dans un contexte donné — pas des rôles de compte distincts.
## Acteurs
| Acteur | Définition |
|---|---|
| **Utilisateur** | Toute personne ayant un compte (un wallet NextGraph). Racine de tous les autres. |
| **Connexion (« ami »)** | Un autre utilisateur avec qui je suis connecté. Sert à scoper les listes (« mes amis qui participent à… ») et la confiance. Bilatérale (acceptation des deux côtés). |
| **Déclarant d'un événement** | L'utilisateur qui a inséré l'événement dans Festipod. *N'est pas forcément l'organisateur réel* : juste celui qui le référence. |
| **Hôte d'un point de rencontre** | L'utilisateur qui a créé un point de rencontre rattaché à un événement. |
| **Inscrit à un point de rencontre** | Un utilisateur inscrit à un point de rencontre ; de fait il devient participant à l'événement parent. |
| **Membre d'une communauté d'intérêt** | Un utilisateur abonné à une communauté pour découvrir les événements qu'elle référence. |
## Concepts métier
| Concept | Définition |
|---|---|
| **Point de rencontre** | *L'unité de valeur de l'app.* Un moment de rencontre proposé par un hôte à un endroit et un horaire donnés, greffé sur un événement public. C'est ce à quoi on s'inscrit. |
| **Événement** | L'ancrage. Un événement public réel référencé dans Festipod pour servir de support à des points de rencontre. Simple prétexte (titre, dates, lieu, thèmes). |
| **Communauté d'intérêt** | Un groupement thématique d'utilisateurs. Sert surtout à découvrir des événements (via abonnement) et à délimiter les périmètres de référencement. |
| **Liste curated** | Une liste d'événements éditorialisée (par un utilisateur ou une communauté), distincte de « les événements que j'ai déclarés ». Permet d'organiser/recommander. |
| **Connexion** | Lien de confiance bilatéral entre deux utilisateurs (équivalent « ami »). |
> Communauté, liste curated et abonnement sont en grande partie **prospectifs** (cf. [[knowledge_roadmap]]). La matrice d'autorisations détaillée par type de donnée vit dans [[brief_2026-05-18_authorization-matrix]].
@@ -0,0 +1,26 @@
---
type: knowledge
summary: Le point de rencontre est l'unité de valeur, greffée sur un événement-prétexte ; on s'inscrit au point de rencontre, pas à l'événement
---
# Modèle métier : le point de rencontre greffé
> Festipod permet aux utilisateurs de créer des **points de rencontre** qui viennent se « greffer » sur des **événements publics existants**. L'objectif : favoriser les rencontres autour de ces événements.
## L'inversion à comprendre
L'**événement public** (festival, conférence, salon, exposition…) n'est **qu'un prétexte** et un *point d'ancrage temporel et géographique*. La valeur produite par l'app, c'est le **point de rencontre** que les utilisateurs viennent y greffer pour se retrouver.
Conséquences directes sur la modélisation :
- **On s'inscrit à un point de rencontre, pas à un événement.** Sans points de rencontre, un événement Festipod n'a aucun intérêt.
- Le **déclarant** d'un événement n'est *pas* (forcément) son organisateur réel — c'est juste quelqu'un qui a inséré la référence dans Festipod pour que d'autres puissent y attacher des points de rencontre.
- L'**hôte** d'un point de rencontre est celui qui l'a créé ; l'acte de créer rend hôte. De même l'acte de déclarer un événement rend déclarant.
## Authentification
**Tous les utilisateurs sont authentifiés** (chacun possède un wallet NextGraph) — il n'y a pas d'accès anonyme à l'app. Les différents « acteurs » (déclarant, hôte, inscrit, connexion…) sont des *spécialisations d'un utilisateur dans un contexte donné*, pas des comptes distincts. Voir [[knowledge_actors-and-concepts]].
## Stack porteuse
App web mobile-first, Bun + React + **NextGraph** (P2P, local-first, chiffré de bout en bout). Le choix P2P a une conséquence métier forte : voir le défi de [[brief_2026-06-15_event-deduplication]].
@@ -0,0 +1,25 @@
---
type: knowledge
summary: Ce qui est implémenté aujourd'hui (cycle événement + point de rencontre, profils, connexions) vs les évolutions identifiées mais non faites (communautés, abonnements, listes curated, multi-user)
---
# Fonctionnalités actuelles vs évolutions à venir
## Implémenté (écrans visibles via le router)
- Authentification via wallet NextGraph
- Cycle de vie d'événement (déclaration, consultation, mise à jour)
- Cycle de vie de point de rencontre (rattaché à un événement)
- Inscription / désinscription à un point de rencontre
- Liste des participants à un événement
- Profil utilisateur, mise à jour, partage de profil
- Liste d'amis (connexions), profil d'un autre utilisateur
> Réserve : certaines actions de données restent des no-ops en l'état (ex. `joinEvent`/`leaveEvent` côté `FestipodDataContext` — détail dans [[brief_2026-05-21_fork-nextgraph-inbox]] §Couche 3). Le router et les écrans existent, mais le branchement données suit le chantier multi-store.
## Évolutions identifiées (non implémentées)
- **Abonnement à une communauté d'intérêt** pour découvrir ses événements (discovery distribué).
- **Abonnement à un utilisateur** pour suivre ses déclarations sans être ami.
- **Listes curated** — créer/partager des sélections éditorialisées.
- **Multi-utilisateurs collaboratif** : aujourd'hui chaque utilisateur a ses données isolées dans son wallet. Le passage collaboratif (un point de rencontre vu par plusieurs) suppose un refactor de la couche données — voir [[brief_2026-05-17_multi-store-refactor]].
@@ -0,0 +1,36 @@
---
type: _overview
summary: NextGraph comme système EXTERNE (stores, permissions, inbox, modèle d'intégration iframe, limites SDK) + les 4 briefs prospectifs qui dérivent la structure de données cible et le chemin multi-user de Festipod
triggers:
keywords: [nextgraph-rs, store, group store, dialog store, protected_store, public_store, inbox, capability, nuri, fork, ngd, broker, verifier, multi-store, multi-user, sharedWalletShim, storeRegistry, permission, social_query, OpenRepo, wallet, auto-import, textcode, wallet import, AccessGateScreen]
paths: ["src/shared/utils/ngGraph.ts", "src/shared/hooks/useShapeWithDefaults.ts", "scripts/build-ng-packages.sh", "src/modules/auth/sharedWallet.ts", "src/modules/auth/screens/AccessGateScreen.tsx"]
---
# NextGraph platform
Deux choses ici, distinctes du concept `data-layer` (qui décrit l'**usage actuel** de NextGraph par l'app) :
1. **Référence du système externe NextGraph** — ses primitives de stockage et de permission, son inbox, son modèle d'intégration/déploiement, et ce que son SDK JS expose (ou pas).
2. **Briefs prospectifs** — la dérivation de la structure de données *cible* de Festipod et les chemins pour y arriver (stopgap wallet partagé, refactor multi-store, fork moteur pour l'inbox).
> Le code de l'app touché par ces chantiers : `src/shared/utils/ngGraph.ts`, `useShapeWithDefaults.ts`, `FestipodDataContext.tsx`, `ngBootstrap.ts` — les seams du futur multi-store. Le modèle de **confidentialité/autorisations** (qui peut faire quoi) vit dans le concept `app-security` ([[brief_2026-05-18_authorization-matrix]]) ; ces chantiers data en sont l'infrastructure.
## Source locale
Le repo `nextgraph-rs` est cloné en `/home/sylvain/projects/nextgraph/nextgraph-rs` (soit `../../nextgraph/nextgraph-rs` depuis la racine projet). À consulter pour vérifier ce qui est réellement exposé au protocole/SDK plutôt que la doc.
## Référence (système externe)
- [[knowledge_stores-permissions]] — 5 types de stores, document/repo, capabilities/Nuri, inbox, exposition SDK JS
- [[knowledge_integration-model]] — paquets JS, modèle iframe, où tourne le verifier, broker `ngd`, déploiement, reciblage build-time
- [[knowledge_broker-import-constraint]] — le broker hébergé n'autorise pas l'auto-import d'un wallet par une web-app tierce (vérifié 2026-06-17)
## Briefs (chantiers prospectifs)
- [[brief_2026-05-17_multi-store-refactor]] — passer du mono-store actuel à une structure par entité
- [[brief_2026-06-15_shared-wallet-shim]] — stopgap staging : wallet partagé unique + `storeRegistry`
- [[brief_2026-05-21_fork-nextgraph-inbox]] — forker `nextgraph-rs` pour exposer l'inbox au SDK JS
## Décisions
- [[decision_2026-06-17_assisted-wallet-import]] — distribution du wallet partagé par import assisté (l'auto-import zéro-touche étant impossible)
@@ -0,0 +1,84 @@
---
type: brief
summary: Passer du mono-store actuel (tout dans private_store) à une structure de stores par entité ; hardcoding dans ngGraph.ts + useShapeWithDefaults ; contrainte SDK bloquante (Group stores/inbox non exposés) ; refactor structurel possible avec placeholders en attendant l'API
last_updated: 2026-05-17
---
# Refactor multi-store NextGraph
**Status:** Incubating — aucun travail démarré
**Last updated:** 2026-05-17
## Context
L'app est aujourd'hui *mono-store* : tout (events, profils, participations, friendships) atterrit dans le `private_store` de l'utilisateur connecté. Héritage de l'exemple expense-tracker-rdf, formalisé dans la décision du 2026-03-17 (concept `data-layer`, [[decision_2026-03-17_private-store-nuri-scope]]).
Ce choix bloque le multi-utilisateurs : le `private_store` est non partageable (*« not possible to share the documents of your private store »*, cf. [[knowledge_stores-permissions]]). Tant que tout y est, Bob ne verra jamais l'event d'Alice. Le modèle natif NextGraph est *multi-store par utilisateur* — Festipod doit s'y aligner avant de devenir collaboratif.
**Déclencheur :** discussion du 2026-05-17 — *poser le cap, exécuter plus tard*.
## What We Know
### État actuel du code
Deux fichiers concentrent le hardcoding du store unique :
- `src/shared/utils/ngGraph.ts` — `ensureGraphNuri()` retourne `did:ng:${session.private_store_id}` pour TOUTES les entités.
- `src/shared/hooks/useShapeWithDefaults.ts` — accepte un `storeNuri` mais l'appelant unique (`FestipodDataContext`) lui passe toujours le NURI du private_store.
Entités impactées (toutes mélangées) : `FpEvent` (→ store partagé), `FpUserProfile` (→ partie privée/publique), `FpParticipation` (→ avec son event), `FpMeetingPoint` (local-only aujourd'hui), `FpFriendship` (local-only, privée). Cf. concept `data-layer` §entités.
### Modèle cible proposé
> **Note (2026-05-19)** : [[brief_2026-05-18_authorization-matrix]] a depuis dérivé, à partir des seuls points validés, une structure différente — 3 stores natifs par utilisateur + Dialog stores, **sans Group store** dans le périmètre actuel. La structure à 4 niveaux ci-dessous reste pertinente pour le périmètre élargi (communautés, collaboration multi-hôte), aujourd'hui hors périmètre. À reconcilier à l'exécution.
Structure hiérarchique en **4 niveaux de Group stores** : index communautaire ⊃ communauté ⊃ event ⊃ meeting point.
| Entité | Store cible | Justification |
|---|---|---|
| Event (métadonnées) | Group « communauté » | La communauté possède l'event → contrôle qui le modifie |
| Référence d'event (pointeur) | Group « index communautaire » | Discovery |
| Participation | Group « event » | N'a de sens que dans son event |
| MeetingPoint (métadonnées) | Group « event » | Le RDV appartient à l'event |
| Participation à un MeetingPoint | Group « meeting point » | RSVP scopé au RDV |
| UserProfile (partie publique) | public_store de l'utilisateur | Modèle natif |
| Friendship | private_store de l'utilisateur | Purement personnelle |
### Contrainte SDK bloquante
Primitives présentes au protocole mais **non exposées dans `@ng-org/web`** (vérifié `0.1.2-alpha.13`) : création de Group stores + invitations/permissions ; **dépôt/lecture d'inbox** (mécanisme retenu pour la notif d'inscription, cf. [[brief_2026-05-18_authorization-matrix]]). `app_request_stream` est la méthode générique la plus susceptible de porter ce mécanisme une fois exposée (à confirmer côté Rust). Cf. [[knowledge_stores-permissions]] §Limites SDK.
**Implication :** le refactor *structurel* peut commencer sans attendre l'API, avec des placeholders (continuer à pointer `private_store_id` pour les Group stores impossibles). L'**aboutissement complet** (vrai multi-user) dépend de l'arrivée de l'API ou d'un contournement (voir [[brief_2026-05-21_fork-nextgraph-inbox]], [[brief_2026-06-15_shared-wallet-shim]]).
### Implications côté code
1. **Disparition de `ensureGraphNuri()`** comme helper unique → helpers par entité ou couche `storeRegistry` résolvant le NURI selon `(entité, contexte)`.
2. **`useShapeWithDefaults` reste un wrapper** mais l'appelant choisit explicitement le store (N appelants demain).
3. **Chaque entité déclare son store cible** (mapping centralisé ou convention shape→store).
4. **`bootstrapWallet()`** (`src/shared/utils/ngBootstrap.ts`) revu : seed réparti, ou seed = données de l'utilisateur courant seulement.
5. **`FestipodDataContext`** : hooks par entité, chacun avec son store résolu.
## Open Questions
1. Quand crée-t-on un Group store de communauté (API absente) ? Acte explicite vs communauté par défaut ?
2. Comment Bob connaît-il l'index communautaire d'Alice ? (possiblement via le public_store d'Alice)
3. Faut-il vraiment 4 niveaux ? Le « meeting point = group store » mérite validation.
4. Que devient le seed de démo quand les Group stores n'existent pas encore ?
5. Migration des wallets de test existants (script / wipe-reseed / ignore) ?
6. Bootstrap d'un user vierge : auto-créer un Group store « par défaut » ou attendre ?
## Possible Approaches
- **Refactor structurel d'abord, partage ensuite** (placeholders `private_store_id`).
- **Registry centralisé** vs **résolution par convention**.
- **Big-bang** vs **par entité** (commencer par Event).
- **Maintenir un mode mono-store** parallèle pour dev/demo.
## Out of Scope
Invitation effective (capability sharing), permissions par rôle, discovery cross-wallet, contournement de l'UI wallet, mode P2P direct sans broker. → second chantier multi-user dont ce refactor est le prérequis structurel.
## Starting Points
- Concept `data-layer` → [[decision_2026-03-17_private-store-nuri-scope]] (la décision qu'on viendra modifier), état du pattern d'écriture
- `src/shared/utils/ngGraph.ts`, `src/shared/hooks/useShapeWithDefaults.ts`, `src/shared/context/FestipodDataContext.tsx`, `src/shared/utils/ngBootstrap.ts`
- NextGraph docs : [Documents et Stores](https://docs.nextgraph.org/en/documents/), [Getting started](https://docs.nextgraph.org/en/getting-started/)
@@ -0,0 +1,96 @@
---
type: brief
summary: Forker temporairement nextgraph-rs pour exposer l'inbox au SDK JS (notif d'inscription, anonymat via from optionnel) — 3 couches : patch Rust (4 fichiers), auto-hébergement ngd+ng-app sur Coolify, intégration Festipod ; fork jetable abandonné quand l'upstream livrera sa solution
last_updated: 2026-05-21
---
# Forker NextGraph pour exposer l'inbox au SDK JS
**Status:** Incubating — aucun travail démarré
**Last updated:** 2026-05-21
## Context
Festipod doit notifier l'hôte d'un PdR quand quelqu'un s'inscrit, avec **identification si connexion / anonyme sinon** (cf. décision cadre inbox dans [[brief_2026-05-18_authorization-matrix]]). L'**inbox** NextGraph est idéale (le `from` optionnel donne l'anonymat) **mais n'est pas exposée au SDK JS** (cf. [[knowledge_stores-permissions]] §Inbox). Ce brief évalue **forker/patcher `nextgraph-rs`** pour l'exposer.
### Posture stratégique (cadrée par l'utilisateur)
Le fork est **explicitement temporaire, non destiné à l'upstream**. Hypothèse : NextGraph finira par exposer sa **propre** solution d'inbox au SDK JS, **possiblement différente**. Quand elle arrivera, on **abandonne le fork et on adapte Festipod**. Tant que leur solution n'est pas là : maintenir le fork à jour (rebase sur `upstream/main`, qui bouge vite en `0.1.2-alpha`) ; **déployer broker + ng-app depuis le fork** ; surveiller l'upstream pour basculer dès que possible. On ne vise **pas** une PR.
## What We Know
Trois couches.
### Couche 1 — Le patch Rust : 4 fichiers (broker vanilla)
1. **`engine/net/src/types.rs`** — `InboxMsgContent::Link` est une variante **unit** (stub) ; lui donner un payload (ou variante `Notification`) portant le NURI du PdR + lien vers l'`Inscription`. Ajouter un builder `InboxPost::new_link(...)` calqué sur `new_contact_details`. `from = None` → anonymat.
2. **`engine/verifier/src/request_processor.rs`** — ajouter le bras de commande manquant (pas de bras `InboxPost`). Idéalement une commande haut-niveau (`NotifyInbox`) construisant le post côté Rust (garde le scellement crypto en Rust). Calquer sur `SocialQueryStart`.
3. **`sdk/js/lib-wasm/src/lib.rs`** — exposer `pub async fn inbox_post_link(session_id, to_inbox_nuri, to_profile_nuri, link, anonymous)`, calqué sur `social_query_start`.
4. **`engine/verifier/src/inbox_processor.rs`** (`process_inbox`) — bras de réception qui **matérialise** le message en document dans le store de l'hôte (calquer sur le handler `ContactDetails`). L'app lit ensuite via ORM/SPARQL — pas de nouvelle API de lecture d'inbox.
**Résolution d'identité** (connu/anonyme) : gratuite via SPARQL côté app (JOIN du NURI d'inbox émetteur contre les docs `social:contact`). **Découverte de l'inbox de l'hôte** : embarquer le NURI d'inbox du `public_store` de l'hôte dans le doc PdR ou le profil public (le flux QR-code de partage de profil le porte déjà).
### Couche 2 — Déploiement (depuis le fork)
Détail dans [[knowledge_integration-model]]. Le verifier patché tourne **dans l'iframe ng-app** → **construire et auto-héberger le `ngd` + le ng-app** depuis le fork, puis rebuilder le `@ng-org/web` de Festipod avec `NG_REDIR_SERVER`/`NG_DEV*` pointant sur ce ng-app. **Aucune réécriture de l'intégration Festipod** (reste iframe). Le routage inbox du broker est déjà natif, mais comme on auto-héberge le ng-app patché, **on déploie toute la stack depuis le fork** (un seul arbre source).
- **Local** : `ngd` + ng-app du fork ; Festipod buildé avec `NG_DEV`/`NG_DEV_LOCAL_BROKER`.
- **Serveur de test** : `ngd` + ng-app du fork sur notre domaine ; Festipod buildé avec `NG_REDIR_SERVER=notre-domaine`.
#### Hébergement sur Coolify — 3 pièces web
1. **`ngd`** — démon WebSocket **stateful** : conteneur avec **volume persistant** pour `--base-path` (RocksDB + clés + PeerId, jamais wipé), mode `--domain` derrière le Traefik de Coolify. Build : Dockerfiles officiels cassés → **écrire notre Dockerfile multi-stage Rust** (RocksDB exige llvm/clang). Premier démarrage **interactif** (lien d'invitation wallet admin) → scripter via `ngcli` ou faire une fois à la main puis persister dans le volume.
2. **ng-app** (frontend iframe, wasm patché) — **build statique** (`pnpm webfilebuild`). Servi en statique (buildpack ou nginx).
3. **Routage** : un même domaine sert le statique du ng-app ET proxifie le WebSocket vers ngd.
Plus **Festipod** lui-même (app Bun → skill `coolify-hosting` pour CELLE-CI, pas pour le `ngd` Rust). Drivers de complexité : build Rust+RocksDB sans Dockerfile prêt, conteneur stateful à volume critique, premier-run interactif, double-service (statique + WS).
### Couche 1 (libs JS) — paquets npm clients patchés
**On maintient des versions patchées des paquets clients, pas seulement le wasm.** Le forwarding générique permet *techniquement* d'atteindre une méthode wasm sans toucher le JS, mais c'est un **hack** (non typé, fragile) — test rapide seulement. À modifier réellement :
- **`@ng-org/web`** — modifié de toute façon (URL broker) → y ajouter `inbox_post_link` dans la **surface d'API typée + `.d.ts`**.
- **Méthodes streamées** (si lecture inbox en *flux* un jour) — entrée des deux côtés (`E` + `streamed_api`). Pour la seule **écriture** (requête/réponse), inutile.
- **`@ng-org/orm`** — à modifier **si** on intègre l'écriture inbox au flux ORM. Sinon (appel `ng.inbox_post_link` à côté), inutile.
- **`@ng-org/alien-deepsignals`, `@ng-org/shex-orm`** — a priori inchangés.
#### Outillage existant : `scripts/build-ng-packages.sh`
`bun run build:ng` build les 4 paquets depuis `$NEXTGRAPH_RS/sdk/js/*` (défaut `../../nextgraph/nextgraph-rs`) → `pnpm pack` → `.tgz` dans `.ng-tarballs/` → `bun add` réécrit `package.json` vers les tarballs locaux. **Pattern d'origine du projet** : le commit `fd6d408` l'a abandonné quand les alphas ont été publiées sur npm. Pour repasser au custom : **réactiver `bun run build:ng`**. Nuances : `@ng-org/web` est TS pur (le script crée un *stub* `lib-wasm` ; le tarball porte l'API inbox typée + l'URL broker bakée, **pas** le wasm) ; pointer le script sur la **branche patchée** (retirer le `git pull --ff-only`) ; option recommandée : patcher `@ng-org/web` pour lire l'URL broker au **runtime** (évite de rebuilder par domaine).
### Couche 3 — Intégration dans Festipod
Exposer la méthode ne suffit pas. Chantiers (certains préexistent à l'inbox) :
- **Modéliser le PdR.** Les SHEX (`src/shared/shapes/shex/festipodShapes.shex`) ne définissent qu'`Event`/`UserProfile`/`Participation` — **pas de `MeetingPoint`** (local-only), ni d'entité notification. Ajouter les shapes + `bun run build:orm`.
- **Implémenter l'inscription (aujourd'hui no-op).** Dans `FestipodDataContext.tsx`, `joinEvent`/`leaveEvent` sont des `console.log`. Le vrai flux : (a) écrire l'`Inscription` dans le `protected_store` de l'inscrit (via multi-store, [[brief_2026-05-17_multi-store-refactor]]), (b) appeler `ng.inbox_post_link(...)` pour notifier l'inbox du PdR.
- **Porter le NURI d'inbox de l'hôte** sur le doc PdR (ou lookup profil).
- **Lire et résoudre les notifications côté hôte** : lire les docs notification matérialisés (ORM/SPARQL), JOIN identité contre `social:contact`. UI : « N inscrits dont X identifiés ».
- **Câblage session** via `src/shared/utils/ngSession.ts`.
**Dépendances** : présuppose (1) le fork SDK livré, (2) le refactor multi-store. **Surface jetable** : à l'arrivée de l'API officielle, migrer aussi ces points d'appel Festipod.
## Open Questions
- `NotifyInbox` haut-niveau vs `InboxPost` brut ? (haut-niveau préféré, garde la crypto en Rust)
- Où sourcer le NURI d'inbox de l'hôte (doc PdR vs lookup profil) ?
- Forme de la matérialisation côté réception (quels triples) ?
- Suppression côté inbox : un déposant peut-il retirer son dépôt ? (résiduelle, cf. [[brief_2026-05-18_authorization-matrix]])
- Cadence de rebase du fork ? Critère de bascule vers la solution upstream ?
- `@ng-org/web` : patch runtime vs tarball par domaine ?
- `ngd` Coolify : automatiser le premier-run vs one-shot manuel persisté ? Un service (reverse-proxy maison) ou deux ?
## Possible Approaches
- **A. Fork temporaire + auto-hébergement (retenu comme stopgap)** — patch des 4 fichiers, déploiement depuis le fork. Vrai inbox, anonymat natif. Coût : maintenir le fork + héberger. Jetable.
- **B. Contribution upstream — écartée** comme objectif.
- **C. Pas de patch, détourner `social_query_start`** — repli, livrable tout de suite mais limité aux **contacts** (pas d'anonyme vers un hôte non-connecté).
> Voir aussi [[brief_2026-06-15_shared-wallet-shim]] : le vrai multi-user (lecture cross-wallet) suppose en plus un patch `OpenRepo` + capabilities, au-delà de l'inbox.
## Starting Points
- [[knowledge_integration-model]], [[knowledge_stores-permissions]]
- [[brief_2026-05-18_authorization-matrix]] — la décision cadre inbox que ce patch sert
- Repo local `nextgraph-rs` : `sdk/js/lib-wasm/src/lib.rs`, `engine/verifier/src/{request_processor,inbox_processor}.rs`, `engine/net/src/types.rs`
- Remotes : `origin` = `git.nextgraph.org/slaivyn/nextgraph-rs` (fork perso), `upstream` = `git.nextgraph.org/NextGraph/nextgraph-rs`
@@ -0,0 +1,183 @@
---
type: brief
summary: Stopgap staging multi-user — deux visions cadrées. Lointaine (cible) — multi-wallet, 3 stores natifs par utilisateur + Dialog, 1 document par entité (événement/PdR), partage par capabilities, inbox native du PdR. Adaptée (stopgap) — UN wallet partagé, 1 document par entité dans son private_store, périmètre = métadonnée logique + index, filtre d'isolation applicatif, login simulé. Obstacles NextGraph — pas de lecture cross-wallet (OpenRepo TODO, ReadCap ignoré), capabilities/inbox non exposées au SDK, login non programmable. sharedWalletShim + filtre = jetables à la migration.
last_updated: 2026-06-16
---
# Stopgap multi-user : wallet partagé unique (`sharedWalletShim`)
**Status:** En cours — compte/login + isolation livrés et vérifiés ; **granularité 1 document par entité implémentée** ; primitives + **fan-out multi-documents validés sur broker** (`@data`, 3 scénarios). Reste : reactivity in-app de la création (best-effort) + seeding multi-doc.
## Objectif & posture
Mettre Festipod en **staging** avec des **utilisateurs amicaux**, **sans enjeu de sécurité**, branché sur le vrai NextGraph, et **structuré au plus près de l'infra cible** pour qu'une migration soit un simple changement de résolveur, pas une réécriture.
Trois choses doivent rester nettes pour ne pas dériver, et structurent ce brief :
1. la **vision lointaine** — ce qu'on aura quand NextGraph offrira lecture cross-wallet, capabilities et inbox ;
2. les **obstacles NextGraph** — ce qui, aujourd'hui, empêche cette vision ;
3. la **vision adaptée** (stopgap) — au plus proche de la lointaine, compte tenu des obstacles.
> **Invariant directeur** : tout ce que fait la vision adaptée doit avoir une **correspondance 1:1** explicite avec la vision lointaine (table en fin de section adaptée). Si un choix du stopgap n'a pas d'image claire dans la cible, c'est un signal de dérive.
---
## 1. Vision lointaine (cible finale)
Dérivée de [[brief_2026-05-18_authorization-matrix]] et [[knowledge_stores-permissions]]. Périmètre **validé** (hors communautés / listes curées / suivi, encore hors périmètre).
### Identité & login
- **1 utilisateur = 1 wallet NextGraph.** Le wallet **est** l'identité ; pas de compte applicatif séparé.
- **Login = ouvrir son propre wallet** (redirect broker). C'est un vrai login par-utilisateur.
### Stores & granularité documents
Modèle natif : `1 document = 1 repo = 1 frontière de permission = 1 inbox`. Un **store** est un document-conteneur qui regroupe et permissionne d'autres documents. Chaque utilisateur a **3 stores natifs** ; les entités sont des **documents individuels** dedans (pas un gros graphe par store).
| Entité | Store (propriétaire) | Granularité | Notes |
|---|---|---|---|
| Événement | `public_store` du déclarant | **1 document / événement** | adressable par NURI (utile pour la déduplication) |
| Point de rencontre (PdR) | `public_store` de l'hôte | **1 document / PdR** | **possède son inbox native** (reçoit les inscriptions) |
| Profil réseau | `protected_store` | 1 document | nom, avatar, bio, ville, intérêts |
| Participation / Inscription | `protected_store` de l'inscrit | 1 document / inscription | + dépôt d'un lien dans l'**inbox du PdR** |
| Profil privé (settings, email) | `private_store` | 1 document | soi seul |
| Connexion A↔B | **Dialog store** A↔B | doc connexion (+ messagerie) | deux écrivains |
| Index des connexions | `protected_store` | 1 document | liste les NURIs des Dialog stores |
**Aucun Group store** sur le périmètre validé (les 3 stores + Dialog + inboxes suffisent).
### Partage & visibilité
- Par **capabilities** : on transmet un Nuri portant un read/write cap. **Public** = lisible par tous sans cap. **Protected** = cap obtenue via la connexion. **Privé** = soi.
- Ajout de permission asynchrone ; retrait synchrone (SyncSignature).
### Inbox & notifications
- L'**inbox native du document PdR** reçoit les dépôts d'inscription (lien DID cap). `from` optionnel ⇒ **identifié si connexion de l'hôte, anonyme sinon**, gratuitement.
### Découverte
- **Pas d'annuaire central.** On découvre via les `public_store` et le graphe de connexions (et plus tard `social_query`).
---
## 2. Obstacles côté NextGraph (ce qui empêche la vision lointaine aujourd'hui)
Vérifiés dans `nextgraph-rs` (2026-06-15).
| Élément de la cible | Obstacle actuel | Preuve |
|---|---|---|
| Lire le store d'un **autre** utilisateur | `OpenRepo` **non implémenté** ; un NURI étranger lève `RepoNotFound` ; une session ne contient que ses 3 stores dans `self.repos` | `engine/verifier/src/verifier.rs:1423`, `request_processor.rs` `resolve_target` |
| Partager une **capability** (Nuri + droits) | non exposé au SDK ; le champ `access`/`ReadCap` du NURI **n'est jamais inspecté** | [[knowledge_stores-permissions]] |
| **Inbox** d'un document (notif d'inscription) | pas exposée au SDK JS (nécessite un fork moteur) | [[brief_2026-05-21_fork-nextgraph-inbox]] |
| **Login per-utilisateur** fluide | login **non programmable** (redirect web vers le broker) | [[decision_2026-06-15_shared-wallet-login-flow]] |
**Conséquence centrale** : tant que la lecture cross-wallet n'existe pas, **aucune donnée ne franchit la frontière entre deux wallets**. Bob ne peut pas lire le `public_store` d'Alice. Toute approche « chacun son wallet » est donc bloquée à la racine.
---
## 3. Vision adaptée (stopgap) — au plus proche de la cible
### Principe : UN wallet partagé
Tous les utilisateurs amicaux ouvrent **le même** wallet. NextGraph ne voit qu'une identité → **tout est techniquement lisible** (on contourne l'absence de lecture cross-wallet en supprimant la frontière). Le « multi-utilisateur » devient une **fiction applicative**.
### Granularité documents — **identique à la cible** : 1 document par entité
Pour rester fidèle, on reproduit les **deux niveaux** de la cible (conteneur → documents) :
- chaque **événement** et chaque **PdR** = **son propre document** (`doc_create`), tous physiquement dans le `private_store` de l'unique wallet partagé ;
- le **périmètre** (public/protected/private) est une **métadonnée logique** portée par le document, **pas** un store physique ;
- un **document-index par (utilisateur × périmètre)** liste les NURIs des entités de ce périmètre — il **joue le rôle du futur store-conteneur** (`docPublic` ≈ futur `public_store`, etc.).
Garder la granularité « 1 doc par entité » est ce qui rend la migration 1:1 **et** ce qui rendra l'**inbox du PdR** possible plus tard sans refonte (l'inbox est un attribut de document).
| Entité | Périmètre logique | Document stopgap | Indexé dans |
|---|---|---|---|
| Événement | public | 1 doc / événement | `docPublic` du déclarant |
| PdR | public | 1 doc / PdR | `docPublic` de l'hôte |
| Profil réseau | protected | doc profil réseau | `docProtected` |
| Participation | protected | 1 doc / participation (ou groupé) | `docProtected` de l'inscrit |
| Profil privé | private | doc settings | `docPrivate` |
| Connexion A↔B | dialog | doc connexion | index de connexions |
### `sharedWalletShim` (échafaudage, sans équivalent cible)
Index des **comptes** simulés → leurs documents-index : `username → profileId → { docPublic, docProtected, docPrivate }`. Ancré dans le `private_store` du wallet partagé (`session.private_store_id`, toujours connu → ancre de bootstrap). Rend possibles le **login cross-device** et le **picker d'utilisateurs**. **N'a aucun équivalent cible** (la cible n'a pas d'annuaire central) → **jetable**.
### Partage simulé : filtre d'isolation applicatif
Un seul wallet ⇒ tout lisible. Pour **se comporter** comme la cible, la couche données filtre les lectures par `currentAccountId` + connexions : `private` → propriétaire ; `protected` → propriétaire + connexions ; `public` → tous. Remplace les **capabilities** (pas appliqué par la crypto, mais honoré par l'app) → **jetable**.
### Identité & login simulés
- **Couche réelle (technique, invisible)** : le redirect broker du wallet partagé, présenté comme **barrière d'accès à l'environnement** (pas un login). Cf. [[decision_2026-06-15_shared-wallet-login-flow]].
- **Couche applicative (le login perçu)** : écran « Connexion » = **username seul** (déclaratif, sans mot de passe) → `localStorage`. « Déconnexion » = efface le username, sans toucher NG. Vrai logout planqué.
### Inbox / notification d'inscription
**Hors périmètre du stopgap** (nécessite le fork). Mais la granularité « 1 doc/PdR » est le **pré-requis** qui la rendra branchable plus tard sans refonte.
### Correspondance stopgap → cible (l'invariant 1:1)
| Stopgap | Vision lointaine | Migration |
|---|---|---|
| doc événement (dans le wallet partagé) | doc événement dans le `public_store` du déclarant | déplacer le doc + appliquer cap publique |
| doc PdR | doc PdR dans le `public_store` de l'hôte **+ inbox** | déplacer + brancher l'inbox |
| `docPublic`/`docProtected`/`docPrivate` (index) | `public_store`/`protected_store`/`private_store` | l'index devient le store natif |
| filtre d'isolation applicatif | capabilities (read caps via connexion) | retirer le filtre, poser les caps |
| login applicatif (username) | login = ouvrir son wallet | retirer la couche compte |
| `sharedWalletShim` | — (rien) | supprimer |
| wallet partagé unique | un wallet par utilisateur | éclater par propriétaire |
**Migration = swap du résolveur `storeRegistry`** (de « doc dans le wallet partagé » vers « doc dans le vrai store du propriétaire ») + déplacement des documents + pose des capabilities + suppression de l'échafaudage. **Les écrans ne changent pas.**
---
## Familles de contournement (pourquoi A)
| Famille | Idée | Verdict |
|---|---|---|
| **A — wallet partagé** | un seul wallet, multi-user simulé côté app | **retenue** : livrable vite, zéro travail moteur, local-first préservé |
| B — NG comme backend | un backend Bun détient un wallet, clients HTTP | écartée : abandonne le local-first, plus lourd |
| C — lecture cross-wallet | chacun son wallet, lecture du public des autres | **infaisable** (obstacle §2) sans fork moteur |
| D — fork moteur (`OpenRepo` + capabilities) | rendre la cible réelle | hors stopgap : c'est le chemin cible, lourd (cf. [[brief_2026-05-21_fork-nextgraph-inbox]]) |
---
## État d'implémentation (2026-06-16)
Deux drapeaux de build, **OFF par défaut** (le mono-store validé reste le défaut ; dev/`@ui`/`@e2e` inchangés) :
- **`FESTIPOD_STAGING=1`** — flux login option 2 (découplé de `NODE_ENV` pour ne pas bloquer `@e2e`).
- **`FESTIPOD_MULTISTORE=1`** — couche multi-document (storeRegistry).
| Pièce | Fichier | État |
|---|---|---|
| Couche compte (faux login, localStorage) | `src/shared/context/AccountContext.tsx` | ✅ livré, vérifié (build + `@ui`) |
| Gate technique + écran « Connexion » + orchestrateur | `src/modules/auth/screens/{AccessGateScreen,ConnexionScreen}.tsx`, `src/app/AuthGate.tsx` | ✅ livré |
| Vrai logout planqué | `ngSession.ts:logoutNg`, `SettingsScreen.tsx` | ✅ livré |
| Filtre d'isolation (mode connecté) | `src/shared/utils/isolation.ts` + `FestipodDataContext` | ✅ livré, pur, vérifié |
| storeRegistry + sharedWalletShim | `src/shared/utils/storeRegistry.ts` | ✅ **1 doc/entité** (`createEntityDoc`/`listEntityDocs` + index par périmètre) ; primitives **validées broker** |
| Câblage multi-document (reads fan-out `{graphs}` + write per-entité `createEntityDoc`) | `FestipodDataContext` (useNgData) derrière `MULTISTORE` | ✅ lecture fan-out validée ; ⚠️ reactivity de la création in-app = **best-effort** (le nouveau doc est ajouté au fan-out, l'`@id` peut être en attente jusqu'au re-subscribe) |
| Validation broker (`@data`) | `src/modules/workshop/{features/multistore-stopgap.feature, steps/data/multistore.steps.ts}` | ✅ **3 scénarios verts** (ORM-sur-doc-créé, shim r/w, **fan-out par entité**) |
**Granularité = 1 document par entité (fait)** : public (événements/PdR) → un `doc_create` **par entité**, NURI ajouté à l'**index** du périmètre (le futur store-conteneur) ; protected (profil, participations) → **groupé** dans l'index protected. Lecture publique = `listEntityDocs('public')` → `useShape({graphs:[…]})`. Mono-store (défaut) inchangé.
**Validation broker (2026-06-16, `@data` contre `nextgraph.net`, 3 scénarios verts)** :
1. ✅ `doc_create("Graph","data:graph","store",undefined)` → NURI utilisable comme `@graph` ORM (write+read d'une `Participation` via `useShape({graphs:[nuri]})`). On n'est **pas** limité au `private_store` comme scope (cf. [[rule_private-store-scope]]).
2. ✅ Shim r/w : 3 docs créés + `sparql_update`, rechargés via `sparql_query` (`readBindings` tolérant, OK en pratique).
3. ✅ **Fan-out par entité** : 2 comptes × 1 doc-événement (`createEntityDoc` + indexé), un `useShape({graphs:[docA,docB]})` lit **les deux** événements, l'index public liste les deux docs.
4. ⏳ **Reste** : reactivity de la création in-app (best-effort, à itérer sur broker) + seeding multi-document (auto-seed neutralisé en `MULTISTORE`).
## Open Questions
- **Modèle d'écriture de l'événement** (propriétaire / wiki / immuable) — *ouvert dans la matrice*. Propriétaire/immuable → événement = doc dans le `public_store` du déclarant (granularité par entité exacte). **Wiki** → exigerait un Group store (hors périmètre) et **changerait la cible**.
- **Création des documents** : `doc_create` à la création de l'entité (retenu par la granularité par entité) ; création paresseuse des index de périmètre.
- **Picker d'utilisateurs** : saisie libre vs liste des comptes du `sharedWalletShim`.
## Out of Scope
- Le **vrai multi-user** (lecture cross-wallet) — fork moteur (`OpenRepo` + capabilities), voir [[brief_2026-05-21_fork-nextgraph-inbox]].
- L'**inbox du PdR** (notif d'inscription) — même fork.
- L'**auto-hébergement** du broker/ng-app (staging sur `nextgraph.net`).
- Toute **sécurité réelle** (credential partagé, pas de mot de passe, pas de chiffrement par utilisateur).
## Starting Points
- [[brief_2026-05-18_authorization-matrix]] — périmètres et partition de la vision lointaine
- [[brief_2026-05-17_multi-store-refactor]] — l'indirection `storeRegistry`
- [[brief_2026-05-21_fork-nextgraph-inbox]] — le chemin cible réel (cross-wallet + inbox)
- [[decision_2026-06-15_shared-wallet-login-flow]] — flux login/logout
- [[knowledge_stores-permissions]] — stores, capabilities, inbox, limites SDK
- `src/shared/utils/storeRegistry.ts`, `src/shared/context/FestipodDataContext.tsx`, `src/app/AuthGate.tsx`, `src/shared/utils/isolation.ts`
- Source `nextgraph-rs` : `sdk/rust/src/tests/sparql_regressions.rs:136-200` (multi-document), `engine/verifier/src/verifier.rs:1423` (TODO `OpenRepo`)
@@ -0,0 +1,51 @@
---
type: decision
summary: Flux login/logout du stopgap wallet partagé — le vrai login NextGraph (redirect broker) apparaît en premier, perçu comme une barrière technique d'accès à l'environnement ; l'écran applicatif « Connexion » (username seul → localStorage) EST le login perçu ; « Déconnexion » efface juste le username sans toucher NG ; vrai logout planqué
last_updated: 2026-06-15
---
# Décision 2026-06-15 — Flux de login/logout du stopgap wallet partagé
Arbitrage du flux d'authentification perçu pour le stopgap [[brief_2026-06-15_shared-wallet-shim]]. Frozen.
## Contrainte de départ
Le login NextGraph **n'est pas programmable** : c'est une **redirection web** vers la page du broker (`nextgraph.net`). Impossible d'ouvrir le wallet partagé en silence — il faut au minimum un passage par le redirect broker, au moins une fois par device. La question n'est donc pas *« comment éviter le redirect »* mais *« comment l'ordonner et le présenter »* pour que l'UX reste cohérente.
## Décision : option 2 — gate technique d'abord, « Connexion » applicative ensuite
Deux couches d'auth distinctes, présentées dans cet ordre :
1. **Couche réelle (technique, non perçue comme login)** — le redirect broker apparaît **immédiatement, avant tout rendu de l'app**. Comme il précède l'app, l'utilisateur le lit comme une **barrière technique d'accès à l'environnement de test** (type mur de beta), **pas** comme un login applicatif. Mêmes credentials partagés pour tous (donnés dans l'invitation, façon « code d'accès »). Une fois par device, puis persistant. **Jamais étiqueté « login ».** Un splash Festipod minimal précède le redirect pour donner du contexte.
2. **Couche applicative (perçue comme LE login)** — écran **« Connexion »** = saisie du **username** (→ `localStorage`, `currentAccountId`). C'est le login *dans la perception* de l'utilisateur. **Sans mot de passe** (décision username-seul) → connexion **déclarative** : n'importe qui prend n'importe quel username (cohérent zéro-sécurité / amis). **« Déconnexion »** = efface **seulement** le username et revient à l'écran « Connexion » ; **n'appelle aucune fonction NG**.
Le **vrai logout** (`ng.session_stop` / `user_disconnect` / `wallet_close`) reste **planqué** (réglages/debug), car il force un nouveau redirect.
Le label **« Connexion »/« Déconnexion »** (et non « Changer de profil ») est un choix explicite : on assume de faire passer le username pour le login applicatif, puisque la barrière technique n'est pas perçue comme tel.
## Pourquoi (vs option 1 écartée)
**Option 1 écartée** — faux login d'abord (username), puis page d'avertissement « saisissez tel username/password », puis bouton *Continuer* déclenchant le redirect. Rejetée : workflow étrange, **double-login dissonant** (« je me suis déjà connecté, pourquoi je recommence ailleurs ? »), page d'avertissement qui **ressemble à une arnaque**, et le redirect **ressurgit en plein usage** à chaque expiration de session.
**Option 2 retenue** parce que :
- **Cohérence du modèle mental** : la barrière technique n'étant pas perçue comme un login, la paire **Connexion/Déconnexion** applicative est complète et auto-cohérente — plus aucun mismatch sur le logout (se déconnecter ramène à l'écran de connexion, les deux dans la même couche).
- **Dégradation gracieuse** : un re-gate après redémarrage navigateur (perte de `sessionStorage`) se lit comme « reconnexion à l'environnement », pas comme un bug.
- **Implémentation plus simple** : `NextGraphContext` fait déjà le flux `connect`/redirect ; l'écran « Connexion » est un écran in-app normal ; pas de page d'avertissement bespoke.
- **Similarité avec l'infra cible** (objectif directeur du stopgap) : la forme **« redirect broker → app »** est exactement le flux du vrai multi-wallet. À la migration, on **supprime l'écran « Connexion » username** et la **barrière technique devient le vrai login per-user** — la forme du flux ne change pas.
## Faits techniques vérifiés (`nextgraph-rs`, 2026-06-15)
- **Persistance de session : OUI.** Wallet mémorisé côté iframe broker (`localStorage` long-terme + `sessionStorage` pour la session active) ; au rechargement, `init()` retrouve la session **sans re-déclencher le redirect** tant que la session broker existe (`sdk/js/web/src/index.ts`, `sdk/js/api-web/main.ts`). Un **redémarrage complet du navigateur** (perte de `sessionStorage`) peut re-déclencher le gate.
- **Logout réel exposé : OUI.** `ng.session_stop()`, `ng.user_disconnect()`, `ng.wallet_close()` (`sdk/js/lib-wasm/src/lib.rs`) ; arrêtent la session / effacent le wallet ; **forcent un nouveau redirect** ensuite → d'où le choix de **ne pas** les appeler dans la « Déconnexion » applicative et de planquer le vrai logout.
## Conséquences côté code (Festipod)
- `NextGraphContext` — déclencher le `connect`/redirect **au boot**, avant le rendu de l'app (+ splash pré-redirect).
- Un écran applicatif **« Connexion »** (username → `localStorage` / `currentAccountId`), username résolu contre les comptes du `sharedWalletShim`.
- Une **« Déconnexion »** qui efface seulement le username (aucun appel NG).
- Vrai logout exposé seulement en réglages/debug.
## See Also
- [[brief_2026-06-15_shared-wallet-shim]] — le stopgap que cette décision complète
- Concept `data-layer` — `NextGraphContext`, auto-init conditionnel, flux redirect broker
@@ -0,0 +1,53 @@
---
type: decision
summary: Distribution du wallet partagé par IMPORT ASSISTÉ PAR FICHIER (.ngw) — l'auto-import zéro-touche étant impossible (broker hébergé), Festipod sert le FICHIER du wallet (téléchargement) + le mot de passe et guide un import unique sur nextgraph.eu « Import a Wallet File » depuis l'AccessGateScreen. Le TextCode a d'abord été retenu puis CORRIGÉ (transfert temporaire 5 min, inutilisable à embarquer). Barrière d'accès ON par défaut (ACCESS_GATE_DISABLED=1 pour bypass tests/dev), ancien LoginScreen retiré. Alternatives écartées (auto-import app, lien magique, self-host) ; provisioning de test (storageState) distinct
last_updated: 2026-06-29
---
# Décision 2026-06-17 — Distribution du wallet partagé par import assisté
Comment un utilisateur récupère le wallet partagé sur un nouveau navigateur, dans le stopgap [[brief_2026-06-15_shared-wallet-shim]]. Frozen.
## Contrainte de départ
L'auto-import zéro-touche par l'app est **impossible** avec le broker hébergé (fait vérifié : [[knowledge_broker-import-constraint]]). Le wallet doit préexister dans le navigateur, importé sur `nextgraph.eu` (cross-origin, non pilotable par Festipod). La question n'est donc pas « comment auto-importer » mais « comment **minimiser la friction de récupération** » — le problème initial étant que l'utilisateur devait d'abord *se procurer* le wallet.
## Décision : import assisté par FICHIER
Festipod **sert le FICHIER `.ngw`** du wallet partagé (téléchargement) et **affiche le mot de passe** dans l'`AccessGateScreen` (la barrière d'accès, cf. [[decision_2026-06-15_shared-wallet-login-flow]]), avec un guide en 3 étapes :
1. Télécharger le fichier du wallet partagé (bouton de téléchargement).
2. Ouvrir `https://nextgraph.eu/#/wallet/login` (nouvel onglet) → « Import a Wallet File » → choisir le fichier → saisir le mot de passe affiché.
3. Revenir et cliquer « Entrer » (redirect broker → demande de déverrouiller le wallet → mot de passe → app).
Festipod **fournit** ainsi le wallet (fin de la friction de récupération) ; l'**import lui-même reste un geste manuel unique par device**, incompressible avec le broker hébergé. Posture **zéro-sécurité, credential partagé** assumée (cf. brief) → embarquer le fichier + le mot de passe est cohérent.
> **Correction 2026-06-25 (le TextCode était une fausse piste)** : la 1ʳᵉ version embarquait le **TextCode**. Or le TextCode est un **transfert temporaire** (5 min, deux devices en ligne, usage unique — cf. [[knowledge_broker-import-constraint]]), donc **inutilisable embarqué** (un testeur arrivant plus tard aurait un code mort). Le test e2e passait quand même car il génère+importe le code dans la foulée. La primitive correcte est le **FICHIER statique**.
## Pourquoi (alternatives écartées)
- **(a) Auto-import embarqué par l'app** — *impossible* : le broker ne laisse aucune fenêtre d'exécution avant son gate wallet ([[knowledge_broker-import-constraint]]).
- **(b) TextCode embarqué** — *cassé* : transfert temporaire 5 min, non réutilisable (cf. correction ci-dessus).
- **(c) Lien magique pré-rempli** vers le broker — pas de route d'import par URL côté broker hébergé.
- **(d) Self-host / fork du ng-app** — seule voie vers le **vrai zéro-touche**, mais lourde ; track séparé ([[brief_2026-05-21_fork-nextgraph-inbox]]). Non retenu pour le stopgap.
## Conséquences côté code (Festipod)
- `src/modules/auth/sharedWallet.ts` — `SHARED_WALLET_PASSWORD` lu depuis un **global gravé au build** `globalThis.__FESTIPOD_SHARED_WALLET_PASSWORD__` (`define` dans `build.ts`, depuis `FESTIPOD_SHARED_WALLET_PASSWORD`) ; `SHARED_WALLET_FILE_URL = /shared-wallet.ngw` ; `hasSharedWallet()` (mot de passe non vide) pilote l'affichage. Vide par défaut → la barrière retombe sur le flux simple.
- `build.ts` — copie le fichier (`FESTIPOD_SHARED_WALLET_FILE`) dans le bundle en `/shared-wallet.ngw` + grave le mot de passe.
- `src/modules/auth/screens/AccessGateScreen.tsx` — section assistée (téléchargement du fichier + mot de passe + guide) affichée si `hasSharedWallet()`.
- `src/app/AuthGate.tsx` — la barrière est **ON PAR DÉFAUT** (« Festipod ne fonctionne jamais sans NextGraph »). **Révision 2026-06-29** : drapeau **inversé** — la barrière n'est désactivée que si `globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ === true` (gravé par `build.ts` depuis `ACCESS_GATE_DISABLED=1`, ou injecté par le harness via `context.addInitScript` pour `@e2e`). Absent → barrière ON. (Remplace l'ancien `FESTIPOD_STAGING`/`__FESTIPOD_REQUIRE_NG__`, qui était OFF par défaut.)
- **Ancien `LoginScreen` retiré** (`/login`, bouton « Se connecter avec NextGraph » + login démo email/mdp) : obsolète puisque l'`AccessGateScreen` précède le routeur. Évite le dead-end « connecter sans wallet ». Route `/login` supprimée.
- **Atterrissage post-login** : après le choix du pseudo, `ConnexionScreen` navigue vers `/home` (la redirection vers l'accueil que faisait l'ancien `LoginScreen` avait disparu avec lui → on retombait sur l'onboarding `WelcomeScreen` à `/`). Filet pour les retours : `WelcomeScreen` redirige vers `/home` si déjà connecté. L'e2e `@humain` va désormais jusqu'à l'accueil pour couvrir ça.
- L'admin exporte le fichier une fois (nextgraph.eu : menu wallet → Download/Export Wallet File) et l'injecte au build (`FESTIPOD_SHARED_WALLET_FILE=<chemin.ngw> FESTIPOD_SHARED_WALLET_PASSWORD=<mdp> bun run build` ; barrière ON par défaut, pas de drapeau à poser).
## À distinguer du provisioning de test
Le harness multi-navigateur provisionne le wallet partagé par **injection storageState** (niveau navigateur, sans la contrainte broker) — concept `bdd-testing` → `knowledge_multibrowser-harness`. Cela **prouve** « wallet partagé → app connectée » mais **court-circuite l'import**. Le mécanisme RÉEL est validé **de bout en bout par la vraie app** (e2e `@humain`) : un navigateur vierge ouvre l'app staging, **télécharge le fichier proposé par l'`AccessGateScreen`** (et vérifie que le mot de passe affiché est celui du wallet), l'importe via le vrai flux `nextgraph.eu` « Import a Wallet File », revient, clique « Entrer » et atteint l'app connectée. Les deux ne se confondent pas.
## See Also
- [[knowledge_broker-import-constraint]] — le fait technique qui force cette décision
- [[brief_2026-06-15_shared-wallet-shim]] — le stopgap
- [[decision_2026-06-15_shared-wallet-login-flow]] — le flux d'accès dont l'AccessGateScreen est la barrière
- [[knowledge_stores-permissions]] — wallet, capabilities, limites SDK
@@ -0,0 +1,69 @@
---
type: knowledge
summary: Le broker NextGraph hébergé n'autorise pas l'auto-import d'un wallet par une web-app tierce — init() top-level redirige, toute méthode ng.* exige d'être déjà loggé dans l'iframe, et le broker renvoie un device sans wallet vers nextgraph.eu (cross-origin, non pilotable). Un wallet doit préexister dans le navigateur. Des 4 méthodes d'import nextgraph.eu, seul le FICHIER .ngw est statique/réutilisable ; le TextCode/QR sont des transferts temporaires (5 min, deux devices en ligne) inutilisables à embarquer.
last_checked: 2026-06-25
---
# Contrainte : pas d'auto-import de wallet par une web-app tierce
Fait technique **vérifié empiriquement (2026-06-17)** : avec le broker NextGraph **hébergé** (`nextgraph.net`), une web-app tierce (Festipod) **ne peut pas** provisionner/importer un wallet par programme. Le wallet doit **préexister** dans le navigateur avant que le redirect d'authentification puisse réussir.
## Pourquoi (mécanisme du proxy `@ng-org/web`)
Lecture de `ngweb.js` (dist de `@ng-org/web`) :
- **`init()` top-level REDIRIGE** : si `window.self === window.top`, il fait `window.location.href = https://nextgraph.net/redir/#/?o=<url>`. Le code de l'app ne tourne plus.
- **Toute méthode `ng.*` est relayée** par `parent.postMessage` vers `nextgraph.net`, et le handler **lève `"you must call init() first"` tant que la session n'est pas établie** (garde interne `d !== false`). Cela inclut `wallet_import_from_code`, `add_in_memory_wallet`, `session_in_memory_start`.
- L'app tierce ne s'exécute **dans l'iframe qu'APRÈS** que le broker a déjà ouvert un wallet et établi la session. **Il n'existe aucune fenêtre** où notre code tourne *avant* le gate wallet du broker → **rien à quoi accrocher un auto-import**.
> Vérifier : `node_modules/@ng-org/web/dist/ngweb.js` — fonction `init` (redirect / postMessage selon top-vs-iframe) et le handler `apply` du Proxy `ng`. Cohérent avec [[knowledge_integration-model]] (le verifier tourne dans l'iframe du ng-app, le proxy ne fait que relayer).
## Ce que montre le broker (probe Playwright, navigateur frais sans wallet)
Sur `https://nextgraph.net/redir/#/?o=...`, le broker affiche **littéralement** :
> « We could not find a wallet in your browser. For now, creating a new wallet while a Web App is authenticating, is not implemented. Please create or import your wallet in a new tab. »
…et renvoie vers `https://nextgraph.eu/` (app wallet **standalone**). L'import standalone (`/#/wallet/login`) propose **4 méthodes**, mais elles ne sont **PAS équivalentes** :
| Méthode | Nature | Embarquable / réutilisable ? |
|---|---|---|
| **Import a Wallet File** (`.ngw`) | **fichier statique** (export portable, hors-ligne) | ✅ **oui** — statique, sans expiration, sans device source |
| Import with TextCode | **transfert temporaire** device↔device via leurs serveurs : **5 min**, **les deux appareils en ligne**, usage unique | ❌ non |
| Import with QR-Code | transfert live (même famille que TextCode) | ❌ non |
| Import via Username | récupération liée à un compte | à étudier |
> **Piège vérifié (2026-06-25)** : le **TextCode n'est PAS un export statique** — l'écran nextgraph.eu le dit (« temporarily stored on our servers for up to 5 minutes », « both devices need to be online »). L'embarquer dans l'app est **inutilisable** : il expire / est à usage unique. Un test automatisé qui génère ET importe le code dans la foulée **passe** (live), masquant le problème — d'où une fausse piste initiale. **La primitive correcte pour un wallet partagé embarqué = le FICHIER `.ngw`.**
## Logique du redirect nextgraph.net (broker discovery) + ORDRE critique
`init()` redirige vers `nextgraph.net/redir`. Là, nextgraph.net regarde dans le **localStorage** s'il connaît un **broker** (le domaine du broker, posé lors d'un import/login wallet antérieur) :
- **trouvé** → redirige vers le broker (`nextgraph.eu/auth/#/wallet/login` → « Click here to login with your wallet <nom> » → mot de passe) → app. **Cas qui marche.**
- **absent** → message « We could not find a wallet in your browser… Please create or import your wallet in a new tab by clicking here ». Le lien ouvre `nextgraph.eu` (page **Welcome**) → **Login** → **Import a Wallet File** → on rejoint l'import.
> **Ordre critique** : il faut **importer le wallet AVANT** d'atteindre nextgraph.net. Le guide de l'`AccessGateScreen` impose cet ordre (télécharger + importer, *puis* « Entrer »).
**Deux routes vers l'import** : **A** = lien direct `nextgraph.eu/#/wallet/login` (celui de Festipod + des tests — saute la page Welcome/Login) ; **B** = fallback nextgraph.net « no wallet → clicking here » → Welcome → **Login** → Import (si on atteint nextgraph.net sans wallet).
**Pourquoi l'e2e `@humain` ne bute pas sur le « no wallet »** : il importe le fichier (route A) **avant** le « Entrer », donc nextgraph.net trouve déjà le broker. Le test **ne couvre pas** la route B (message « no wallet » + page Welcome/Login de nextgraph.eu) — ce sont des comportements nextgraph, pas Festipod, mais un humain cliquant « Entrer » en premier y tombe.
> **Atténuation côté Festipod (2026-06-29)** : la barrière `AccessGateScreen` (qui **fournit** le fichier wallet + le mot de passe + le guide) est désormais l'écran d'entrée **par défaut** (cf. [[decision_2026-06-17_assisted-wallet-import]]) — l'ancien `LoginScreen` « Se connecter avec NextGraph » (qui menait directement au redirect sans fournir le wallet) a été retiré. Un utilisateur ne peut donc plus atteindre nextgraph.net **sans** que Festipod lui ait d'abord proposé le wallet. La route B reste possible s'il clique « Entrer » avant d'importer, mais il a le wallet sous les yeux pour le faire.
> **Contrainte UX irréductible + dé-piégeage** : « Entrer » fait une **redirection pleine-page**. Si on clique AVANT d'importer → message « no wallet » → l'import se fait dans un **autre onglet** sans retour auto vers Festipod (le broker hébergé ne sait pas revenir). Il faut donc **importer d'abord, PUIS Entrer** (guide de l'`AccessGateScreen`, ordre du `@humain`). Piège corrigé (2026-06-29) : au retour (back) après un « Entrer » prématuré, la page standalone était restaurée du bfcache avec l'état figé sur `connecting` → bouton « Accès en cours » bloqué ; `NextGraphContext` écoute `pageshow.persisted` (sans session) et réinitialise sur `disconnected` pour permettre de réessayer.
## Pistes d'élimination du va-et-vient — testées, ÉCARTÉES (2026-06-30)
Deux idées pour éviter le 2ᵉ onglet ; les deux **infaisables** sans fork :
1. **Embarquer `nextgraph.eu` en iframe** dans Festipod pour guider l'import sur le même écran. nextgraph.eu **n'a pas** d'en-tête anti-framing (embed possible, import OK dans l'iframe), **MAIS** le wallet importé atterrit dans le stockage **partitionné** `(top: festipod, frame: nextgraph.eu)` — invisible du login top-level → « no wallet ». **Confirmé en vrai navigateur (Chrome).** ⚠️ **Le Chromium de Playwright N'applique PAS ce partitioning → faux positif** : un probe Playwright montrait le wallet « transmis », alors que le vrai Chrome bloque. Ne jamais valider une question d'**isolation de stockage** via Playwright ; tester en vrai navigateur.
2. **Déclencher l'écriture cross-origin sur nextgraph.net** : le mécanisme existe (pont `/auth` iframe+postMessage qui écrit `ng_bootstrap` sur nextgraph.net pendant le login nextgraph.eu) mais ce sont **les pages de NextGraph** qui l'orchestrent ; un tiers ne peut pas écrire le localStorage d'une autre origine (same-origin policy), et ça ne couvrirait que la découverte du broker, pas le wallet.
→ Seule élimination réelle = self-host/fork du ng-app ([[brief_2026-05-21_fork-nextgraph-inbox]]). Le flow stopgap reste l'import **en onglet séparé** (top-level `nextgraph.eu`, première-partie → pas de partitioning → fonctionne).
## Conséquences
- Le wallet doit être importé **sur `nextgraph.eu` (cross-origin)** — Festipod ne peut **pas** piloter ce flux ni pré-remplir l'import (pas de route d'import par URL côté broker hébergé).
- Le **vrai zéro-touche** exigerait de **self-host/forker le ng-app** (territoire de [[brief_2026-05-21_fork-nextgraph-inbox]]).
- Distribution produit retenue, vu cette contrainte : **import assisté par FICHIER** — Festipod fournit le `.ngw` (téléchargement) + le mot de passe et guide l'import — voir [[decision_2026-06-17_assisted-wallet-import]].
- À ne pas confondre avec le **provisioning de TEST** (injection storageState dans le harness), qui n'a pas cette contrainte car il agit au niveau navigateur — concept `bdd-testing` → `knowledge_multibrowser-harness`.
@@ -0,0 +1,51 @@
---
type: knowledge
summary: Modèle d'intégration NextGraph — @ng-org/web est un proxy iframe (verifier tourne dans l'iframe ng-app, pas dans le broker), reciblable au build via NG_REDIR_SERVER/NG_DEV*, broker ngd stateful WebSocket ; modifier le verifier = rebuilder le ng-app, pas le broker
last_checked: 2026-05-21
---
# Modèle d'intégration et de déploiement NextGraph
Comment une app web tierce s'intègre à NextGraph, et **où tourne le moteur (verifier)**. Vérifié dans `nextgraph-rs` le 2026-05-21.
NextGraph s'utilise via un **proxy iframe** (`@ng-org/web`) : l'app tierce ne contient pas le moteur, elle délègue à un ng-app hébergé (défaut `nextgraph.net`) qui exécute le moteur dans une iframe.
## Les paquets JS
- **`@ng-org/web`** — paquet **publié**. Proxy postMessage léger (aucun wasm embarqué). **Le** chemin d'intégration tierce ; `@ng-org/orm` et tous les exemples en dépendent. **Festipod l'utilise.**
- **`@ng-org/api-web`** — **privé** (non publié). Moteur navigateur complet (charge `@ng-org/lib-wasm` dans un Web Worker). Consommé uniquement par `app/nextgraph` (frontend ng-app) — **pas** une cible d'intégration tierce.
- **`@ng-org/lib-wasm`** — moteur compilé wasm (contient le verifier). Source `sdk/js/lib-wasm/`.
- **`nextgraph`** (npm) — API NodeJS (build `pkg-node`).
- **`@ng-org/orm`** — ORM réactif (`useShape`…), bâti sur `@ng-org/web`.
## Où tourne le verifier
Dans le modèle web standard (iframe), le verifier tourne **dans l'iframe** : `app/nextgraph` charge `api-web` → `lib-wasm` dans un Web Worker, côté navigateur. Le broker (`ngd`) ne fait que **transport et stockage**.
**Conséquence** : modifier la logique du verifier (`request_processor`, `inbox_processor`) = reconstruire le **ng-app**, pas le broker.
## Le modèle iframe & reciblage build-time
`@ng-org/web` redirige vers le ng-app hébergé, qui recharge l'app tierce en iframe après auth, puis relaie par `postMessage`. **Reciblable au build** (`sdk/js/web/src/index.ts`, `import.meta.env`) :
| Variable | Cible |
|---|---|
| `NG_REDIR_SERVER` | défaut `nextgraph.net` |
| `NG_DEV3` | `127.0.0.1:3033` |
| `NG_DEV` | `localhost:14402`/`14404` |
| `NG_DEV_LOCAL_BROKER` | `localhost:1421` |
**Pas d'override runtime** — `init()` ne prend pas d'URL broker. Pour pointer vers un ng-app auto-hébergé : **rebuilder `@ng-org/web`** (TS pur, sans wasm → build trivial).
## Plomberie proxy ↔ iframe ↔ worker (générique)
Le chemin d'appel d'une méthode est **entièrement générique** (aucune allowlist) : `@ng-org/web` est un `Proxy` JS qui relaie *n'importe quel* nom de méthode par `postMessage` ; `app/nextgraph` dispatch via `Reflect.apply(ng[method], …)`. **Conséquence** : une nouvelle fonction wasm en requête/réponse simple est *atteignable* sans toucher le JS — mais c'est un **hack** non typé (test rapide, pas un plan ; cf. [[brief_2026-05-21_fork-nextgraph-inbox]]). Cas **streamé** : exige une entrée des deux côtés (`E` dans `@ng-org/web` + `streamed_api` dans api-web ; méthodes streamées actuelles : `doc_subscribe`, `orm_start_graph`, `orm_start_discrete`, `file_get`, `app_request_stream`).
## Le broker (ngd)
- Supporte déjà nativement l'inbox (`inbox_post`, `inbox_register`, `inbox_pop_for_user` dans `engine/net/src/server_broker.rs`) — un `ngd` standard routerait l'inbox, **aucun patch broker nécessaire**.
- Démon **WebSocket** (`async-tungstenite`), **stateful** : RocksDB sous `--base-path`, PeerId persisté (volume critique).
- CLI : `--local PORT`, `--domain DOMAIN:PORT,LOCAL_PORT` (mode derrière reverse-proxy TLS-terminé type Traefik/Coolify).
- **Ne sert pas de statique** : le ng-app frontend est un déploiement statique séparé (`pnpm webfilebuild`). Premier démarrage **interactif** (lien d'invitation wallet admin). Dockerfiles officiels **cassés**.
> Détail du déploiement depuis un fork : [[brief_2026-05-21_fork-nextgraph-inbox]] §Couche 2.
@@ -0,0 +1,60 @@
---
type: knowledge
summary: Référence des 5 types de stores NextGraph et leurs droits, document=repo, granularité des permissions, capability/Nuri, inbox native (anonymat via from optionnel), et ce que le SDK @ng-org/web n'expose PAS
last_checked: 2026-05-21
---
# Stores NextGraph et droits d'accès
Référence des primitives de stockage et permission de NextGraph (**système externe**, pas le code de Festipod). Socle des briefs [[brief_2026-05-17_multi-store-refactor]] et [[brief_2026-05-18_authorization-matrix]].
Source : doc NextGraph officielle ([Documents & Stores](https://docs.nextgraph.org/en/documents/), [Getting started](https://docs.nextgraph.org/en/getting-started/)) vérifiée le 2026-05-21.
## Points d'entrée du code source local
Repo cloné en `../../nextgraph/nextgraph-rs` (cf. `_overview`) :
- `sdk/js/lib-wasm/src/lib.rs` — API wasm effectivement exposée au JS.
- `engine/net/src/app_protocol.rs` — enum `AppRequestCommandV0`, formats `NuriV0`.
- `engine/verifier/src/request_processor.rs` — dispatch effectif des `app_request` (la vérité sur ce qui est *traité*).
- `engine/net/src/types.rs` — types inbox (`InboxPost`, `InboxMsg`, `InboxMsgContent`).
- `engine/verifier/src/inbox_processor.rs` — traitement des messages d'inbox.
## Les 5 types de stores
| Store | Lecture | Écriture | Création |
|---|---|---|---|
| **Private** | Titulaire seul | Titulaire seul | Par défaut |
| **Protected** | Titulaire + détenteurs d'un lien + permission | Titulaire + collaborateurs permissionnés | Par défaut |
| **Public** | Tout le monde, sans capability | Titulaire seul | Par défaut |
| **Group** | Membres du groupe | Membres du groupe (collaboratif) | À la demande |
| **Dialog** | Les deux utilisateurs uniquement | Les deux utilisateurs uniquement | À la demande |
Citations doc (verbatim) : Private — *« only you have access to … not possible to share »* ; Protected — *« share … but they will need a special link and permission »*, *« protected social profile »* ; Public — *« equivalent to your website … without the need for special permissions »* ; Group — *« each Group is a separate Store … documents inherit the permissions of the store »* ; Dialog — *« hold all the data you exchange with another user (and only with that other user) … You cannot add more users »*.
Tout wallet a d'office les **3 stores** private/protected/public (session : `private_store_id`, `protected_store_id`, `public_store_id`). Group et Dialog se créent à la demande.
## Concepts transverses
**Document vs Repo.** *« A Repo is the equivalent of an E2EE group for one and only one Document. »* **1 document = 1 repo** (commits + permissions). Identifiant : `did:ng:o:<RepoID>`. Un **store** est lui-même un document spécial qui regroupe et permissionne d'autres documents.
**Granularité.** Écriture gérée au niveau **Document (repo)**, pas branche/bloc. Lecture plus fine possible (par bloc/branche). Héritage : un Group store peut faire hériter ses permissions à ses documents.
**Capability / Nuri.** Le partage transmet un **Nuri** embarquant la capability crypto (lecture et/ou écriture). Pas d'ACL centralisée : posséder le Nuri = le droit. *« adding permissions can be done offline »* ; *« removing permissions … requires a SyncSignature »* (synchrone).
## Inbox
**Chaque document a une inbox native.** Un non-éditeur peut y **déposer un lien (DID cap)** sans être invité éditeur ; le propriétaire **modère**. NURI : `did:ng:d:<inbox_id>`. Contenu : enum `InboxMsgContent` (`ContactDetails`, `DialogRequest`, **`Link`**, `Patch`, `ServiceRequest`, `ExtRequest`, `RemoteQuery`, `SocialQuery`…). Message **scellé** (`crypto_box::seal`) vers la pubkey de l'inbox → seul le titulaire déchiffre. Champ `from` **optionnel** → expéditeur **anonyme** possible. C'est le « identifié si connu, anonyme sinon » voulu par Festipod, **natif au protocole** (mécanisme retenu pour la notification d'inscription, cf. [[brief_2026-05-18_authorization-matrix]]).
### L'inbox n'est PAS utilisable directement depuis le SDK JS
- `app_request(request)` est exposé, et `AppRequestCommandV0::InboxPost` + `AppRequest::inbox_post()` existent. **MAIS** le `request_processor` du verifier **n'a aucun bras `InboxPost`** (commandes traitées : `OrmStart(Discrete)`, `Fetch`, `FileGet`, `OrmUpdate`, `OrmDiscreteUpdate`, `SocialQueryStart`, `QrCodeProfile(Import)`, `Header`, `Create`, `FilePut`). Envoyer un `InboxPost` ne déclenche rien.
- Construire un `InboxPost` exige le scellement crypto côté Rust ; **aucun helper wasm** ne l'expose.
- Le dépôt en inbox n'est déclenché qu'**en interne** par `QrCodeProfileImport` (`post_to_inbox(new_contact_details)`) et `social_query_start` (propagation via inbox des **contacts**).
**Conséquence** : pas de moyen propre de « drop a Link » arbitraire dans l'inbox d'un PdR depuis le SDK JS aujourd'hui. → chantier [[brief_2026-05-21_fork-nextgraph-inbox]]. Piste connexe : `social_query_start` EST exposé (requête fédérée via inbox jusqu'à `degree` sauts) mais limité aux **contacts** (ne couvre pas la notif anonyme vers un hôte non-connecté).
## Limites du SDK JS
`@ng-org/web` (vérifié `0.1.2-alpha.13` = `upstream/main` au 2026-05-21, version installée) **n'expose pas** : création de Group/Dialog store ; partage de capability (Nuri avec droits) ; manipulation de permissions ; dépôt/lecture d'inbox.
Méthodes JS disponibles : `doc_create`, `doc_subscribe`, `sparql_query`, `sparql_update`, `orm_start_graph`, `orm_start_discrete`, `graph_orm_update`, `discrete_orm_update`, `file_get`, `app_request_stream`. La doc annonce *« An API will be provided for permission manipulation »* (sans date).
+21
View File
@@ -0,0 +1,21 @@
---
type: _overview
summary: Stack et outillage — Bun-first (runtime, bundler, APIs natives), build pipeline, et commandes du projet
triggers:
keywords: [bun, bunx, build, bundler, vite, webpack, jest, npm, storybook, "bun.serve", hmr, tailwind, package.json]
paths: ["build.ts", "package.json", "bunfig.toml", "tsconfig.json", "src/index.ts", "src/index.html", ".storybook/**", "scripts/**"]
---
# Tech stack
Stack et outillage du projet. Principe directeur : **Bun-first** — Bun remplace Node/npm/vite/webpack/jest et fournit les APIs serveur natives.
**À lire en premier :** [[rule_bun-first]] — la convention qui décide quel outil utiliser.
## Liens
- [[rule_bun-first]] — utiliser Bun, pas Node/npm/vite/jest/express/ws/pg…
- [[knowledge_bun-apis]] — APIs natives Bun (serve, sqlite, redis, sql, file, shell)
- [[knowledge_build-pipeline]] — build.ts, bundler, serveur, harness buildé à part, Storybook
- [[knowledge_stack-and-commands]] — composants de la stack + scripts réels (+ quirks)
- [[knowledge_deployment]] — Dockerfile, prod depuis src/, pas de CI, `portless` en dev
@@ -0,0 +1,25 @@
---
type: knowledge
summary: Dev en bun --hot, build prod via build.ts (bundler Bun + plugin Tailwind) vers dist/, alias @/* → ./src/*
---
# Build pipeline
- **Dev** : `bun --hot src/index.ts` (via `bun run dev`) — HMR, port 3000.
- **Prod** : `bun run build` → `build.ts` (bundler Bun + plugin Tailwind) → `dist/`.
- **Alias de chemin** : `@/* → ./src/*` (déclaré dans `tsconfig.json`).
Le serveur sert `src/index.html`, qui charge `src/app/frontend.tsx` (voir `app-architecture` §app-shell). Le bundler transpile le TSX et bundle le CSS sans outil externe — pas de Vite/webpack/esbuild (cf. [[rule_bun-first]]).
## Détails de `build.ts` et du serveur
- `build.ts` scanne `src/**/*.html` comme entrypoints (aujourd'hui un seul : `src/index.html`), `target: 'browser'`, minify + sourcemap linked, plugin `bun-plugin-tailwind`. Ajouter un 2e `.html` créerait un 2e bundle.
- `src/index.ts` (`Bun.serve`) sert : `/reports/cucumber` (rapport HTML), des stubs `/api/hello*`, et un **catch-all `/*` → `src/index.html`** (routing SPA, doit rester en dernier). HMR si `NODE_ENV !== 'production'`, port via `PORT`.
## Le harness de test est buildé à part
⚠️ `build.ts` ne build **pas** les harness de test. Les hooks Cucumber (`src/shared/support/hooks.ts`) lancent un `bun build` **à la demande** pour `src/shared/test-harness/harness.tsx` (et `harness-ng.tsx`) → `dist/test-harness*.js`. C'est un entrypoint séparé du build app — voir concept `bdd-testing`.
## Storybook
`storybook dev -p 6006` — **webpack5 + SWC** (pas Vite). Les décorateurs (`.storybook/`) injectent la pile complète de providers (Theme > NextGraph > FestipodData > Router) et importent `src/index.css` ; viewport mobile par défaut. Couplage dur au contexte projet (pas réutilisable hors Festipod).
@@ -0,0 +1,41 @@
---
type: knowledge
summary: APIs natives Bun utilisées par le projet — Bun.serve (HTTP/WS/routes), HTML imports bundlés, bun:sqlite, Bun.redis, Bun.sql, Bun.file, Bun.$
---
# APIs natives Bun
Référence des APIs Bun à privilégier (cf. [[rule_bun-first]]). Doc complète : `node_modules/bun-types/docs/**.mdx`.
## Serveur — `Bun.serve()`
Supporte WebSockets, HTTPS et routes. Pas besoin d'`express`/`ws`.
```ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": { GET: (req) => new Response(JSON.stringify({ id: req.params.id })) },
},
websocket: { open: (ws) => ws.send("hello"), message: (ws, m) => ws.send(m), close: (ws) => {} },
development: { hmr: true, console: true },
})
```
C'est le mécanisme de `src/index.ts` (voir concept `app-architecture` §app-shell).
## HTML imports (frontend)
`Bun.serve()` sert des HTML imports ; le bundler Bun transpile/bundle automatiquement `.tsx`/`.jsx`/`.js` et le CSS (Tailwind inclus). Un `<script type="module" src="./frontend.tsx">` dans le HTML suffit — pas de Vite.
## Stockage & shell
- **`bun:sqlite`** pour SQLite (pas `better-sqlite3`)
- **`Bun.redis`** pour Redis (pas `ioredis`)
- **`Bun.sql`** pour Postgres (pas `pg`/`postgres.js`)
- **`WebSocket`** intégré (pas `ws`)
- **`Bun.file`** plutôt que `node:fs` readFile/writeFile
- **`Bun.$\`ls\`** plutôt qu'`execa`
Bun charge `.env` automatiquement → ne pas utiliser `dotenv`.
@@ -0,0 +1,28 @@
---
type: knowledge
summary: Déploiement — Dockerfile multi-stage Bun Alpine qui lance `bun run start` depuis src/ (pas dist/), EXPOSE 3000, env PORT/NODE_ENV ; aucun CI/CD committé ; dev passe par le wrapper portless
last_checked: 2026-06-15
---
# Déploiement & infra
## Dockerfile
Un `Dockerfile` existe (multi-stage Bun Alpine) :
- `FROM oven/bun:1-alpine`, stages `install` (`bun install --frozen-lockfile` depuis `package.json` + `bun.lock`) puis `release` (copie `node_modules` + source).
- `ENV NODE_ENV=production`, `USER bun`, `EXPOSE 3000/tcp`, `ENTRYPOINT ["bun","run","start"]`.
**Quirk** : `start` = `NODE_ENV=production bun src/index.ts` → le conteneur **exécute la source TypeScript directement** (Bun transpile à la volée), il **n'utilise pas `dist/`**. Le `bun run build` (→ `dist/`) n'est donc **pas** sur le chemin de prod par défaut. Si on veut servir le build, il faut changer l'entrypoint.
## CI/CD
**Aucun** pipeline committé (`.github/workflows/` absent, pas de config Coolify dans le repo). Angle mort assumé. Pour héberger l'app Bun, le skill `coolify-hosting` s'applique (mentionné aussi dans concept `nextgraph-platform` pour distinguer Festipod du `ngd` Rust).
## Variables d'environnement
- `PORT` (défaut 3000), `NODE_ENV` (active/désactive HMR et l'auto-seed dev — cf. concept `data-layer`).
- Aucun `.env*` committé (`.env` est gitignored). Pas de gestion de secrets dans le repo.
## Dev
`bun run dev` = **`portless festipod bun --hot src/index.ts`** — passe par le wrapper **`portless`** (outil externe de gestion de port), pas un `bun --hot` nu. HMR actif hors production.
@@ -0,0 +1,40 @@
---
type: knowledge
summary: Composants de la stack (Bun, React, NextGraph, Storybook, Cucumber, Tailwind-dans-le-build) et liste réelle des scripts package.json, dont les quirks (cucumber via node+tsx, build:orm au chemin périmé, build:ng pour le fork local)
---
# Stack & commandes
## Composants
| Couche | Techno |
|---|---|
| Runtime / bundler / test | **Bun** (cf. [[rule_bun-first]]) |
| UI | **React** (mobile-first, largeur max 768px — style dans concept `app-architecture`) |
| Données | **NextGraph** P2P local-first (concept `data-layer`) |
| Build CSS | **Tailwind** (`tailwindcss` + `bun-plugin-tailwind`) — présent dans le build, mais les écrans stylent via `app-*`/inline, pas d'utilitaires Tailwind (cf. concept `app-architecture`) |
| Exploration UI | **Storybook** (webpack5 + SWC, port 6006) |
| Tests | **Cucumber/Gherkin** FR multi-couches + Playwright + happy-dom + chai (concept `bdd-testing`) |
## Scripts `package.json` (réels)
| Script | Commande / rôle |
|---|---|
| `dev` | `portless festipod bun --hot src/index.ts` — dev HMR via wrapper `portless` (cf. [[knowledge_deployment]]) |
| `start` | `NODE_ENV=production bun src/index.ts` — prod, depuis `src/` (pas `dist/`) |
| `build` | `bun run build.ts` — bundler Bun + Tailwind → `dist/` ([[knowledge_build-pipeline]]) |
| `test:cucumber` | enchaîne `cucumber:run` → `cucumber:report` → `features:parse` → `steps:extract` |
| `cucumber:run` | `node --import tsx/esm …/cucumber-js` — **via Node+tsx, pas Bun** (compat plugins Playwright/happy-dom) |
| `test:data` | idem `--tags @data` |
| `test:auth-setup` | `bun scripts/setup-test-auth.ts` — bootstrap wallet de test persistant |
| `cucumber:report` | `bun scripts/parse-test-results.ts` — `cucumber-report.json` → HTML |
| `features:parse` | `bun scripts/parse-features.ts` → `features.ts` |
| `steps:extract` | `bun scripts/extract-step-definitions.ts` |
| `build:orm` | `rdf-orm build --input ./src/shapes/shex --output ./src/shapes/orm` |
| `build:ng` | `bash scripts/build-ng-packages.sh` — rebuild des `@ng-org/*` depuis le fork local (concept `nextgraph-platform`) |
| `storybook` / `build-storybook` | Storybook dev (6006) / build statique |
## Pièges
- **`cucumber:run`/`test:data` tournent sous Node+tsx**, pas Bun — les plugins de test ne chargent pas en import Bun natif. Ne pas « bunifier » ces scripts.
- **`build:orm` cible `./src/shapes/shex` et `./src/shapes/orm`**, alors que les shapes réelles vivent sous **`src/shared/shapes/`** — le chemin du script est vraisemblablement **périmé** (à corriger ou exécuter avec les bons chemins ; vérifier avant de régénérer l'ORM).
@@ -0,0 +1,31 @@
---
type: rule
summary: Par défaut utiliser Bun et ses APIs natives, jamais les équivalents Node — bun au lieu de node/ts-node, bun install/test/build, bunx, et pas d'express/ws/pg/dotenv
---
# Règle : Bun-first
Par défaut, utiliser **Bun** et ses APIs natives plutôt que les équivalents Node.js.
| Au lieu de… | Utiliser |
|---|---|
| `node <file>`, `ts-node` | `bun <file>` |
| `jest`, `vitest` | `bun test` |
| `npm/yarn/pnpm install` | `bun install` |
| `npm run <script>` | `bun run <script>` |
| `npx <pkg>` | `bunx <pkg>` |
| `webpack`, `esbuild`, `vite` | `bun build` / bundler Bun (HTML imports) |
| `express` | `Bun.serve()` |
| `better-sqlite3` | `bun:sqlite` |
| `ioredis` | `Bun.redis` |
| `pg`, `postgres.js` | `Bun.sql` |
| `ws` | `WebSocket` (intégré) |
| `node:fs` readFile/writeFile | `Bun.file` |
| `execa` | `Bun.$\`...\`` |
| `dotenv` | (inutile — Bun charge `.env` automatiquement) |
Détail des APIs : [[knowledge_bun-apis]].
## Pourquoi
Le projet est tout-Bun (runtime, bundler, test, serveur). Réintroduire un outil Node redondant ajoute une dépendance, divergerait des conventions du repo, et casse l'intégration native (HMR, transpilation TS automatique, chargement `.env`). C'est un choix de cohérence, pas une préférence cosmétique.
+36
View File
@@ -0,0 +1,36 @@
# Inter-repo contracts. Festipod is a CONSUMER only: it publishes no interface of its own,
# and what it lists under `publish:` is its own consumer-side DECLARATIONS — the channel a
# provider pulls to learn what this app calls, what it needs, and what blocks it.
#
# Festipod's persistence sits behind interchangeable Refine backends (the backend contract
# itself is in-code: `src/shared/refine/providers/types.ts`). Each candidate backend is a
# separate interface, with its own folder inside `data-layer` — the concept that owns how
# Festipod uses an external data surface.
#
# `@activitypods/refine-providers` publishes NO engagement: no contract document, no stated
# change policy, not even a git tag. So its entry is `type: reconstruct` — the one bounded
# crossing this protocol allows, made once at adoption time: the leaf under `into:` is OUR
# observation of that package, authored here and pinned to the commit it was read at. It is
# not a promise anyone made us, and every upgrade has to re-observe it.
#
# Once reconstructed, that leaf IS the specification this app codes against. Reading the
# package's sources again at work time is not how a question gets answered — the leaf is
# fixed or the gap goes into our declaration's `## Needs`.
publish:
# Key qualified by interface: a second declaration is coming for the NextGraph+polyfill
# backend, and publish keys must stay unique.
activitypods-providers--festipod: concepts/data-layer/activitypods-providers/usage_festipod.md
consume:
- contract: activitypods-providers
into: concepts/data-layer/activitypods-providers/
type: reconstruct
# Canonical identity: the package's git remote plus the repo-relative folder the
# observation was made from. Per-developer access to a checkout of it lives in
# `.project/contracts.local.yaml`, which is never committed.
reconstructFrom: https://github.com/activitypods/refine-providers.git/src
# The branch the observation was read from. A tag would be the better pin, but this
# repository publishes none — the resolver turns this into the commit actually read,
# which is what `built_version` records.
ref: master
@@ -1,54 +0,0 @@
# Automated Headless Wallet Creation for CI
**Date:** 2026-03-12 15:00
**Status:** Accepted
## Context
Data-layer BDD tests (`@data` scenarios) require a NextGraph wallet in a persistent Chromium profile. Previously, the first run required manual interaction: a visible browser opened and the user had to create a wallet and close the browser. This blocked CI execution.
## Options Considered
### Option A: Programmatic wallet creation via NG SDK
Call `ng.wallet_create()` directly from Node/Bun, bypassing the UI entirely.
**Arguments for:**
- Fastest execution
- No browser needed for wallet creation
**Arguments against:**
- `@ng-org/web` is browser-only (WASM + postMessage)
- Would need to reverse-engineer the registration API at `account.nextgraph.eu`
- Doesn't test the real auth flow
### Option B: Automate the browser UI flow headlessly
Use Playwright to drive the same wallet creation UI a real user would use, but in headless mode.
**Arguments for:**
- Tests the real auth/login feature end-to-end
- No API reverse-engineering needed
- Same persistent profile used for subsequent test runs
- CI-ready with no manual steps
**Arguments against:**
- Depends on `nextgraph.eu` and `account.nextgraph.eu` being reachable
- UI changes in NextGraph could break the automation
- Adds ~27s to first run
## Decision
Option B — automate the browser UI. The wallet creation flow (navigate to `nextgraph.eu` → "Create Wallet" → accept ToS at `account.nextgraph.eu` → fill username/password → submit) is itself a legitimate test of the app's auth feature. The dependency on external services is acceptable since the tests already depend on the broker being reachable.
## Consequences
**Positive:**
- Tests are fully CI-ready (no human interaction)
- Auth/login flow is tested as a side effect
- Single command `bun run test:data` works from a clean state
**Negative:**
- Requires internet access (nextgraph.eu, account.nextgraph.eu)
- Fragile to NextGraph UI changes (button text, form IDs)
**Risks:**
- `account.nextgraph.eu` rate limiting could block CI runs that frequently recreate wallets
@@ -1,48 +0,0 @@
# Conditional NextGraph Init Based on Broker Iframe Detection
**Date:** 2026-03-13 14:00
**Status:** Accepted
## Context
`@ng-org/web`'s `initNgWeb()` checks `window.self === window.top`. When the app runs standalone (not in an iframe), it redirects the entire page to `nextgraph.net/redir/` to trigger broker authentication. This caused the app to redirect on every load — even during development or when the user hadn't clicked "Se connecter".
## Options Considered
### Option A: Always auto-init NG on mount
**Arguments for:**
- Simpler code — no branching logic
**Arguments against:**
- Causes immediate redirect to broker when loaded standalone
- Breaks development workflow
- User sees broker login page instead of the app
### Option B: Conditional auto-init based on iframe detection
**Arguments for:**
- When in iframe, the broker has already authenticated — safe to auto-init
- When standalone, user must explicitly click "Se connecter" to trigger the redirect
- Preserves standalone demo/development experience
- Matches `@ng-org/web`'s own detection logic
**Arguments against:**
- Relies on `window.self !== window.top` heuristic (could theoretically be wrong if embedded in non-broker iframe)
## Decision
Option B. `NextGraphContext` checks `const isInsideBroker = typeof window !== 'undefined' && window.self !== window.top` at module level. `useEffect` only auto-calls `initNg()` when `isInsideBroker` is true. The `connect()` callback remains available for explicit user-initiated connection.
Additionally, `FestipodDataContext` now renders empty data (not seed data) during the `connecting` phase to avoid flashing demo content before the wallet loads.
## Consequences
**Positive:**
- App loads without redirecting — works standalone for development and demo
- In broker iframe, connection is seamless and automatic
- No seed data flash during wallet connection
**Negative:**
- None significant
**Risks:**
- If `@ng-org/web` changes its detection logic, our guard may diverge — keep them aligned
@@ -1,67 +0,0 @@
# Use private_store_id as useShape scope and @graph
**Date:** 2026-03-17 16:00
**Status:** Accepted
## Context
Clicking "Charger données de test" loaded data in-memory (via ORM signals) but produced `RepoNotFound` errors from `doc_create` and `orm_frontend_update`. Data disappeared after page reload because SPARQL writes never reached the broker. The NextGraph verifier's `self.repos` HashMap didn't contain the private store repo, so `resolve_target()` failed.
## Options Considered
### Option A: `did:ng:i` scope + `doc_create` for @graph
Use "entire user site" scope for reads, create a new document for writes.
**Arguments for:**
- `did:ng:i` is well-documented as a valid subscription scope
- `doc_create` returns a real document NURI
**Arguments against:**
- `did:ng:i` uses a special code path (`NuriTargetV0::UserSite`) that doesn't open individual repos
- `doc_create` calls `resolve_target(NuriTargetV0::PrivateStore)` which needs the repo in `self.repos` — fails if repo wasn't opened
- Requires complex retry logic / timing workarounds
### Option B: `private_store_id` as both scope AND @graph
Mirror the expense-tracker-rdf example: `useShape(type, `did:ng:${session.private_store_id}`)` and `@graph: `did:ng:${session.private_store_id}``.
**Arguments for:**
- Proven pattern: expense-tracker-rdf uses exactly this and works
- `orm_start_graph` with private store NURI opens the repo in the verifier's `self.repos` HashMap
- Subsequent writes via `orm_frontend_update` find the repo because it's now in the cache
- Simple, no retry logic needed
**Arguments against:**
- Slightly less flexible than `did:ng:i` (scoped to one store)
- Requires passing session to `useShapeWithDefaults`
### Option C: `did:ng:i` scope + reuse existing entity @graph
Subscribe with `did:ng:i`, then reuse `@graph` from any existing entity for writes.
**Arguments for:**
- Works for returning users who already have data
**Arguments against:**
- Fails for empty wallets (no existing entities to reuse)
- Still needs `doc_create` fallback which hits the same `RepoNotFound` issue
## Decision
**Option B**: Use `did:ng:${session.private_store_id}` as both `useShape` scope and `@graph` for writes. This matches the official expense-tracker-rdf example exactly.
The `useShapeWithDefaults` hook accepts a `storeNuri` parameter. `FestipodDataContext.useNgData()` gets the session from `useNextGraph()` and passes `did:ng:${session.private_store_id}`.
`ensureGraphNuri()` simplified: checks existing entities first (optimization), then falls back to `did:ng:${session.private_store_id}`.
## Consequences
**Positive:**
- Writes work immediately after connection (no retries needed)
- Data persists across page reloads
- Pattern matches official NextGraph examples
- All 7 e2e scenarios pass including data persistence
**Negative:**
- `useShapeWithDefaults` signature changed (added `storeNuri` parameter)
**Risks:**
- If NextGraph changes the private store behavior, this would break
@@ -1,81 +0,0 @@
# Use SPARQL DELETE instead of ORM ngSet.delete() for object removal
**Date:** 2026-03-17 18:00
**Status:** Accepted
## Context
Leaving an event requires deleting the user's `Participation` object from the NextGraph store. The ORM's `DeepSignalSet.delete()` method updates the local reactive state (UI reflects the change immediately) but the deletion does not persist to the broker — after page refresh, the participation reappears.
## Options Considered
### Option A: ORM `ngSet.delete(item)`
The ORM README shows `dogs.delete(aDog)` as the intended API. Internally, `.delete()` generates a `{ op: "remove", path: "/<syntheticId>" }` patch, delivered via microtask to `OrmSubscription.onSignalObjectUpdate`, which calls `ng.graph_orm_update()`.
**Arguments for:**
- Official ORM API, shown in README examples
- Immediate local reactive update (instant UI feedback)
**Arguments against:**
- Does not persist in practice: `delete()` returns `true`, local set updates, but after refresh the object is back
- The `graph_orm_update` WASM call may not correctly handle "remove" patches for top-level set objects (possible engine bug)
- No error is thrown — fails silently
### Option B: `ng.sparql_update()` with SPARQL DELETE
Bypass the ORM patch mechanism entirely. Use `DELETE WHERE { GRAPH <graph> { <subject> ?p ?o } }` to remove all RDF triples for the object.
**Arguments for:**
- Works: deletion persists across page refresh
- The broker confirms via `TORMO became invalid` + `GraphOrmUpdate` remove, which reactively removes the item from the ORM set
- Direct control over RDF triple removal
**Arguments against:**
- Not instant: UI update waits for the SPARQL round-trip + broker `GraphOrmUpdate` callback (near-instant in practice, ~50ms)
- Must not combine with `ngSet.delete()` — running both causes CRDT conflicts where the item reappears
### Option C: `ngSet.delete()` + `ng.sparql_update()` together
Use `.delete()` for instant UI and SPARQL for persistence.
**Arguments for:**
- Instant UI feedback + guaranteed persistence
**Arguments against:**
- **Does not work**: the ORM `.delete()` patch and the SPARQL DELETE backend update conflict in the CRDT, resulting in neither UI change nor persistence
## Decision
**Option B: SPARQL DELETE only.** The broker sends back a `GraphOrmUpdate` with `op: "remove"` that reactively removes the item from the ORM set, so the UI still updates — just not synchronously.
Do NOT call `ngSet.delete()` alongside `sparql_update()` — they conflict.
## Implementation
```typescript
// In FestipodDataContext.tsx leaveEvent():
const session = await sessionPromise;
await ng.sparql_update(
session.session_id,
`DELETE WHERE { GRAPH <${partGraph}> { <${partId}> ?p ?o } }`,
partGraph,
);
```
Imports: `ng` from `@ng-org/web`, `sessionPromise` from `../utils/ngSession`.
## Consequences
**Positive:**
- Deletion actually persists
- Single source of truth (broker → ORM → UI)
**Negative:**
- Slight UI delay (~50ms) vs instant for property mutations
- Pattern diverges from ORM README examples
**Risks:**
- If `ng.sparql_update` API changes, this breaks
- Other delete operations (if added) must follow the same pattern
- The ORM `ngSet.delete()` bug may be fixed in a future version — revisit when upgrading `@ng-org/orm`
-84
View File
@@ -1,84 +0,0 @@
# Architecture
Feature-based architecture where code is organized by business domain (module), not by technical layer.
## Module Structure
```
src/modules/
event/ # 7 screens, 5 features — events CRUD, discovery, participants, meeting points
user/ # 5 screens, 11 features — profiles, friends, sharing
home/ # 2 screens — dashboard, settings
auth/ # 2 screens — login, welcome/onboarding
workshop/ # 0 screens, 6 features — workshop/atelier specs (future)
meeting/ # 0 screens, 1 feature — meeting point specs
notification/ # 0 screens, 3 features — notification specs
```
Each module can contain:
- `screens/` — React screen components
- `features/` — Gherkin `.feature` files (BDD specs)
- `steps/{ui,data,e2e}/` — Cucumber step definitions by layer
## Import Rules
**Modules only import from `shared/` — never from each other.**
```
src/modules/event/screens/EventDetailScreen.tsx
✅ import from '../../../shared/components/sketchy'
✅ import from '../../../shared/context/FestipodDataContext'
✅ import from '../../../screens' (registry types)
❌ import from '../../user/screens/...'
```
## Shared Layer
`src/shared/` contains everything reusable across modules:
| Directory | Contents |
|-----------|----------|
| `components/sketchy/` | Hand-drawn UI library (Button, Card, Avatar, Header, NavBar, etc.) |
| `components/ui/` | Shadcn/Radix components (used only in prototyping tool) |
| `context/` | ThemeContext, NextGraphContext, FestipodDataContext |
| `data/` | User stories (`index.ts`), auto-generated `features.ts`, `testResults.ts`, `seedData.ts`, `types.ts` |
| `hooks/` | `useShapeWithDefaults` (NextGraph) |
| `shapes/` | SHEX definitions + ORM TypeScript bindings |
| `utils/` | `ngSession.ts`, `ngBootstrap.ts` |
| `steps/ui/` | Shared BDD step definitions (navigation, screen, form) |
| `support/` | Cucumber `world.ts`, `hooks.ts` |
| `types/` | `gherkin.ts` (ParsedFeature, ParsedScenario types) |
| `lib/` | `utils.ts` (cn helper for Tailwind) |
## App Shell
`src/app/` is the prototyping tool — not part of the Festipod app itself:
- `App.tsx` — Root: ThemeProvider > NextGraphProvider > FestipodDataProvider > RouterProvider
- `router.tsx` — Hash-based routing: `#/` (gallery), `#/demo/{screenId}`, `#/specs/{featureId}`
- `frontend.tsx` — React entry point (referenced from `src/index.html`)
- `components/Gallery.tsx` — Screen preview grid
- `components/DemoMode.tsx` — Interactive mockup viewer with sidebar navigation
- `components/specs/` — BDD specs browser (SpecsPage, FeatureView, GherkinHighlighter)
## Screen Registry
`src/screens/index.ts` is the central registry that imports all screens from all modules and exports:
- `screenGroups` — Grouped by domain (Accueil, Evenements, Utilisateur, General)
- `screens` — Flat list
- `getScreen(id)` — Lookup by ID
- `ScreenProps` interface — `{ navigate: (screenId: string) => void }`
## Entry Points
| File | Purpose |
|------|---------|
| `src/index.ts` | Bun.serve() — HTTP server, serves index.html + cucumber report |
| `src/index.html` | HTML entry, loads `src/app/frontend.tsx` |
| `src/app/frontend.tsx` | React root, renders `<App />` |
## Build
- Dev: `bun --hot src/index.ts` (via `bun run dev`)
- Prod: `bun run build.ts` — Bun bundler + Tailwind plugin → `dist/`
- Path alias: `@/*` → `./src/*` (tsconfig)
-113
View File
@@ -1,113 +0,0 @@
# BDD Testing
Cucumber/Gherkin BDD specs in French with multi-layer step definitions.
## Overview
- 26 feature files (US-1 to US-26), all in French
- Categories: EVENT, WORKSHOP, USER, MEETING, NOTIF
- Priorities: 0 (Impossible), 1 (Haute), 2 (Moyenne), 3 (Basse)
- Current results: 51 passed, 7 failed, 75 skipped (133 scenarios total)
## Multi-Layer BDD
Each module has step directories for three test layers:
```
src/modules/event/steps/
ui/ # UI/screen assertions (source analysis)
data/ # Data layer assertions (Playwright + broker)
e2e/ # E2E assertions (Playwright + broker + real app UI)
```
Shared steps (cross-domain) live in `src/shared/steps/ui/`.
## Feature Files
Collocated with their module:
```
src/modules/event/features/us-13-creer-evenement.feature
src/modules/user/features/us-23-connexion-utilisateurs.feature
src/modules/workshop/features/us-1-visualiser-atelier-termine.feature
...
```
Tagged with `@CATEGORY @priority-N` for filtering.
## Step Definitions
### Shared Steps (`src/shared/steps/ui/`)
| File | Purpose |
|------|---------|
| `navigation.steps.ts` | Screen navigation, authentication, click/select actions, section/button/field assertions |
| `form.steps.ts` | Form field validation, required fields, import/duplicate detection |
| `screen.steps.ts` | Screen content assertions (participants, events, profiles, QR codes) |
### How UI Steps Work
`@ui` steps render the screen with `LocalDataProvider` (seed data) and `RouterProvider` via happy-dom, then assert on the rendered DOM. The render helper lives in `src/shared/test-harness/renderHelper.tsx` and is invoked from `world.ts:renderCurrentScreen()` on every `navigateTo(...)`.
See [test-layer-contracts](./test-layer-contracts.md) for what `@ui` is allowed to test and the patterns to follow (and avoid).
Legacy: `screenFileMap`, `screenFieldDetectors`, `screenExpectedContent`, `screenRequiredFields` in `world.ts` are vestiges of an earlier source-code-grep approach. `hasText`/`hasField`/`hasElement` now prefer the rendered DOM and fall back to source so unmigrated steps keep working during the transition.
### Screen Name Resolution
French names in `.feature` files map to screen IDs via `screenNameMap`:
- `"accueil"` → `home`
- `"détail événement"` → `event-detail`
- `"mon profil"` → `profile`
- `"relayer un événement"` → `create-event`
## Cucumber Configuration
`cucumber.json`:
```json
{
"default": {
"import": [
"src/shared/support/**/*.ts",
"src/shared/steps/**/*.ts",
"src/modules/*/steps/**/*.ts"
],
"paths": ["src/modules/*/features/**/*.feature"],
"language": "fr"
}
}
```
Requires `tsx` loader: `node --import tsx/esm node_modules/.bin/cucumber-js`
## Auto-Generated Files
Scripts in `scripts/` parse features and steps into TypeScript data files consumed by the prototyping tool:
| Script | Input | Output |
|--------|-------|--------|
| `parse-features.ts` | `src/modules/*/features/*.feature` | `src/shared/data/features.ts` |
| `parse-test-results.ts` | `reports/cucumber-report.json` | `src/shared/data/testResults.ts` |
| `extract-step-definitions.ts` | `src/shared/steps/ui/*.ts` | `src/shared/data/stepDefinitions.ts` |
Run all: `bun run test:cucumber`
## Data-Layer Testing
`@data` scenarios test through the real NextGraph broker. See [data-layer-testing](./data-layer-testing.md) for full architecture.
## E2E Testing
`@e2e` scenarios test the real app running in the broker iframe. See [data-layer-testing](./data-layer-testing.md#e2e-layer) for architecture. Key differences from `@data`:
- Uses the **real app** (not a test harness) served on a local HTTP port
- Interacts via Playwright locators and `evaluate()` on the app iframe
- Tests actual UI behavior: navigation, redirects, button clicks, screen content
- Requires real broker mode (fails with `Error` if broker unavailable)
## Adding New Steps
1. **Module-specific**: Create in `src/modules/{module}/steps/ui/`
2. **Cross-domain**: Add to `src/shared/steps/ui/`
3. Import `FestipodWorld` type from `../../support/world` (shared) or adjust relative path
4. Run `bun run steps:extract` to regenerate tooltip data
-177
View File
@@ -1,177 +0,0 @@
# Data-Layer Testing
BDD scenarios tagged `@data` test the real NextGraph data pipeline through a broker, not mocked data.
## Overview
`@data` scenarios run Cucumber steps against a real NextGraph broker. Playwright drives a Chromium instance that authenticates with the broker, which loads our test harness in an iframe. The harness uses real `useShape`/ORM subscriptions and exposes a `window.__testData` bridge for step definitions.
## Architecture
```
Cucumber steps → Playwright (Chromium, persistent profile)
↓
https://nextgraph.eu/auth/#/?o=http://127.0.0.1:{port}
↓
Broker wallet login (automated)
↓
Broker loads app in iframe → http://127.0.0.1:{port}
↓
harness-ng.tsx (init → useShape → ORM → broker)
↓
window.__testData bridge
```
## Dual Mode
- **Real broker** (default): `harness-ng.tsx` with NextGraph ORM through broker iframe
- **Mock fallback**: `harness.tsx` with standalone DeepSignalSets (if NG harness build fails)
## Wallet Lifecycle
Fully automated — no manual interaction required. CI-ready.
### First Run (wallet creation + bootstrap)
1. `BeforeAll` detects no `.wallet-ready` marker in `.playwright-profile/`
2. Launches headless Chromium with persistent profile
3. Navigates to `https://nextgraph.eu/` → clicks "Create Wallet"
4. Redirected to `account.nextgraph.eu` → clicks "I accept" (ToS)
5. Redirected back → fills username/password form → submits
6. Wallet created in localStorage
7. **Logs in to the wallet** — this triggers the verifier bootstrap from the remote broker, populating localStorage with repo data
8. Waits 10s for bootstrap to complete, then closes context
9. Marker written
Step 7 is critical: the NextGraph verifier starts with an empty `repos` HashMap. On first login, `verifier.sync()` bootstraps from the remote broker, downloading repo data (including store repos). This data is saved to localStorage via `session_save`. Without this initial login, subsequent sessions would have empty repos and all writes would fail with `RepoNotFound`.
### Subsequent Runs (automated login)
1. Marker found → skip wallet creation
2. Headless Chromium with persistent profile
3. Automated login: click "Login" → click wallet link → fill password → submit
4. Broker authenticates, loads app harness in iframe
5. Harness initializes NG, creates ORM subscriptions, seeds data if needed
6. `window.__testData.ready` → steps execute via `appFrame.evaluate()`
### Wallet Credentials
- Name: `festipod-tests`
- Password: `festipod-tests`
## Key Technical Details
### Chromium Flags
```
--disable-features=PrivateNetworkAccessRespectPreflightResults,BlockInsecurePrivateNetworkRequests,...
--allow-insecure-localhost
--disable-web-security
```
Required because broker at `nextgraph.eu` (public) loads harness from `http://127.0.0.1:{port}` (local) in an iframe — Chromium's Private Network Access blocks this by default.
### Persistent Profile (`.playwright-profile/`)
- Stores NG wallet in localStorage (`ng_wallets` on `nextgraph.eu`, `ng_bootstrap` on `nextgraph.net`)
- Gitignored
- Must use full Chrome binary, not `chrome-headless-shell`
### HTTP Server
- Started in `BeforeAll` on auto-assigned port (`127.0.0.1:0`)
- Serves harness HTML at `/` and JS bundle at `/harness.js` (separate files — inline script breaks due to special characters in bundle)
- Shut down in `AfterAll`
### ORM Subscriptions
Harness creates subscriptions for all three shapes with scope `did:ng:${session.private_store_id}` (opens the store repo for reads AND writes):
- `FpEventShapeType` → events
- `FpUserProfileShapeType` → users
- `FpParticipationShapeType` → participations
### Test Bridge (`window.__testData`)
Exposed by the harness, consumed by steps via `appFrame.evaluate()`:
- `events`, `users`, `participations` — live DeepSignalSets
- `currentUserId` — IRI of the test user
- `getEvent(id)`, `getEventByTitle(title)` — lookups
- `joinEvent(eventId, userId)`, `leaveEvent(eventId, userId)` — mutations
- `isParticipating(eventId, userId)`, `getEventParticipants(eventId)` — queries
- `updateEvent(eventId, updates)` — field updates
## E2E Layer (`@e2e`)
`@e2e` scenarios test the real app UI running inside the broker iframe. Unlike `@data` which loads a test harness, `@e2e` loads the actual app.
### Architecture
```
Cucumber steps → Playwright (Chromium, persistent profile)
↓
https://nextgraph.net/redir/#/?o=http://127.0.0.1:{appPort}
↓
Broker wallet login (automated, same as @data)
↓
Broker loads REAL APP in iframe → http://127.0.0.1:{appPort}
↓
App renders with NextGraphProvider auto-connecting
↓
Steps interact via appFrame.evaluate() and Playwright locators
```
### App Server
Started in `BeforeAll` alongside the harness server:
1. Find a free port
2. `spawn('bun', ['src/index.ts'], { env: { PORT: appPort } })`
3. Poll until the server responds to HTTP GET
4. Killed in `AfterAll`
### Shared Infrastructure
`@e2e` reuses the same `setupBrokerPage()` helper as `@data` — handles broker redirect URL construction, wallet login automation, and iframe discovery.
### Step Definitions
E2E steps live in module directories (e.g., `src/modules/auth/steps/e2e/connexion.steps.ts`). They use:
- `this.appFrame!.evaluate()` — run JS in the app iframe (hash navigation, content checks)
- `this.appFrame!.locator()` — find and interact with DOM elements
- `this.appFrame!.waitForFunction()` — poll for expected state (screen content, URL changes)
- `SCREEN_MARKERS` — map screen IDs to unique text content for verification
### Before Hook (`@e2e`)
```
1. Open new Playwright page
2. setupBrokerPage(page, realAppUrl) → automated login → find app iframe
3. Wait for React render (root.innerHTML.length > 100)
4. Wait 3s for NG connection + provider stabilization
```
### Differences from `@data`
| Aspect | `@data` | `@e2e` |
|--------|---------|--------|
| What loads in iframe | Test harness (`harness-ng.tsx`) | Real app (`src/index.ts`) |
| Ready signal | `window.__testData.ready === true` | `root.innerHTML.length > 100` |
| Interaction | `evaluate()` on test bridge | `evaluate()` + Playwright locators |
| Mock fallback | Yes (standalone DeepSignalSets) | No — requires real broker |
| Tests | Data operations (CRUD, queries) | UI behavior (navigation, redirects, clicks) |
## Files
| File | Purpose |
|------|---------|
| `src/shared/test-harness/harness-ng.tsx` | Real broker harness (useShape through broker iframe) |
| `src/shared/test-harness/harness.tsx` | Mock harness (DeepSignalSets, no broker) |
| `src/shared/support/hooks.ts` | Playwright lifecycle (wallet creation, login automation, iframe detection, app server) |
| `src/shared/support/world.ts` | World with `page`/`appFrame` fields |
| `src/modules/event/steps/data/inscription.steps.ts` | Inscription data steps |
| `src/modules/auth/steps/e2e/connexion.steps.ts` | Auth/connection e2e steps |
| `.playwright-profile/` | Persistent Chromium profile (gitignored) |
| `scripts/debug-browser.ts` | Manual browser debug tool — launches headed Chromium to inspect broker interactions |
| `.playwright-profile-debug/` | Chromium profile created by debug-browser.ts (gitignored) |
## Commands
```bash
bun run test:data # Run @data scenarios (real broker if wallet exists, mock fallback)
bun run test:cucumber # Run all scenarios (UI + data + e2e)
```
## See Also
- [BDD Testing](./bdd-testing.md) — general Cucumber setup, UI-layer steps
- [Data Layer](./data-layer.md) — NextGraph stack, shapes, context providers
-115
View File
@@ -1,115 +0,0 @@
# Data Layer
NextGraph-backed local-first data with fallback to local state for demo/disconnected mode.
## Overview
The app has two data modes:
1. **Connected** — NextGraph ORM shapes (P2P, encrypted, local-first)
2. **Disconnected/Demo** — Local React state seeded from `seedData.ts`
All screens use `useFestipodData()` hook regardless of mode.
## NextGraph Stack
```
@ng-org/web # Browser WASM runtime
@ng-org/orm # RDF shape-based ORM
@ng-org/shex-orm # SHEX → TypeScript code generation
@ng-org/alien-deepsignals # Reactive signals bridge
```
Packages installed from npm (`@ng-org/*` alpha versions). For local development against an unreleased `nextgraph-rs` build, `scripts/build-ng-packages.sh` packs the monorepo into `.ng-tarballs/` and updates `package.json` to point at those paths.
## SHEX Shapes
`src/shared/shapes/shex/festipodShapes.shex` defines:
- **Event** — title, description, dates, location, themes, participants
- **UserProfile** — name, username, bio, city, visibility
- **Participation** — links event + user, confirmation status
ORM bindings in `src/shared/shapes/orm/`:
- `festipodShapes.schema.ts` — Schema registration
- `festipodShapes.shapeTypes.ts` — Shape type constants
- `festipodShapes.typings.ts` — TypeScript interfaces
Regenerate with `bun run build:orm`.
## NextGraph Read/Write Pattern
The app follows the same pattern as the official expense-tracker-rdf example:
- **Scope**: `useShape(shapeType, `did:ng:${session.private_store_id}`)` — opens the private store repo in the verifier
- **@graph**: `did:ng:${session.private_store_id}` — writes target the same NURI
This is critical: `orm_start_graph` with the private store NURI explicitly opens the repo in the verifier's `self.repos` HashMap. Without this, `orm_frontend_update` fails with `RepoNotFound`.
**Do NOT use `did:ng:i` as scope** — it subscribes to the entire user site via a special code path that doesn't open individual repos, breaking all writes.
### Deleting Objects
`ngSet.delete(item)` updates the local reactive set but does **not** persist to the broker. Use `ng.sparql_update()` with SPARQL DELETE instead:
```typescript
import { ng } from '@ng-org/web';
import { sessionPromise } from '../utils/ngSession';
const session = await sessionPromise;
await ng.sparql_update(
session.session_id,
`DELETE WHERE { GRAPH <${item["@graph"]}> { <${item["@id"]}> ?p ?o } }`,
item["@graph"],
);
```
The broker sends back a `GraphOrmUpdate` with `op: "remove"` that reactively removes the item from the ORM set. **Do NOT combine with `ngSet.delete()`** — the two operations conflict in the CRDT.
See [decision record](../decisions/2026-03-17-1800-sparql-delete-for-orm-objects.md) for details.
### Key files
- `src/shared/hooks/useShapeWithDefaults.ts` — Accepts `storeNuri` param, passes to `useShape`
- `src/shared/utils/ngGraph.ts` — `ensureGraphNuri()` returns `@graph` for entity creation
- `src/shared/utils/ngBootstrap.ts` — Seeds test data using `ensureGraphNuri()` for `@graph`
See [decision record](.project/decisions/2026-03-17-1600-private-store-nuri-scope.md) for why.
## Context Providers
### NextGraphContext (`src/shared/context/NextGraphContext.tsx`)
- Connection lifecycle: `disconnected` → `connecting` → `connected` | `error`
- Provides session with store IDs (private, protected, public)
- **Conditional auto-init**: Only auto-calls `initNg()` when running inside the broker iframe (`window.self !== window.top`). Outside the iframe, `initNgWeb()` would redirect the page to the broker — so connection waits for explicit `connect()` call.
- `connect()`: Called by user clicking "Se connecter". When outside broker, triggers the redirect flow.
#### `@ng-org/web` redirect behavior
`initNgWeb()` checks `window.self === window.top`. If the app is NOT in an iframe, it redirects to `nextgraph.net/redir/` with the current URL encoded as a return parameter. The broker then loads the app back in an iframe after auth. This means the app must NOT auto-init NG when loaded standalone.
### FestipodDataContext (`src/shared/context/FestipodDataContext.tsx`)
- Wraps NextGraph shapes with `useShapeWithDefaults()` hook
- CRUD: `createEvent()`, `updateEvent()`, `joinEvent()`, `leaveEvent()`, etc.
- Exposes `useFestipodData()` hook consumed by all screens
- `selectedEventId` state for cross-screen event navigation
- `loadTestData()`: Calls `bootstrapWallet()` to seed test data into NG wallet — only triggered by explicit user action
- **Provider states based on NG status**:
- `disconnected` → `LocalDataProvider` with seed data (demo mode)
- `connecting` → `LocalDataProvider` with **empty data** (avoids flashing seed data before wallet loads)
- `connected` → `NgDataProvider` with real wallet data
- `error` → `LocalDataProvider` with seed data (graceful fallback)
## Data Types
`src/shared/data/types.ts`:
- `FpEventData` — id, title, date, location, distance, themes, etc.
- `FpUserData` — id, name, username, bio, city, counts
- `FpParticipationData` — eventId + userId + confirmed
- `FpMeetingPointData` — eventId, location, time, host (local-only)
- `FpFriendshipData` — userId + friendId (local-only)
## Seed Data
`src/shared/data/seedData.ts`:
- 10 users (Marie Dupont = current user, `user-1`)
- Multiple events with dates, locations, themes
- Participations, meeting points, friendships
- `CURRENT_USER_ID = 'user-1'`
-94
View File
@@ -1,94 +0,0 @@
# Screens
16 mobile mockup screens using the sketchy hand-drawn component library.
## Screen Inventory
### Home Module (`src/modules/home/screens/`)
| ID | Name | File | Description |
|----|------|------|-------------|
| `welcome` | Bienvenue | WelcomeScreen.tsx | Onboarding/welcome page |
| `home` | Accueil | HomeScreen.tsx | Dashboard with upcoming events, quick actions |
| `settings` | Parametres | SettingsScreen.tsx | Notifications, privacy, location settings |
### Event Module (`src/modules/event/screens/`)
| ID | Name | File | Description |
|----|------|------|-------------|
| `events` | Decouvrir | EventsScreen.tsx | Event discovery/search |
| `event-detail` | Detail evenement | EventDetailScreen.tsx | Event info, participants, join/leave |
| `create-event` | Relayer evenement | CreateEventScreen.tsx | Create/relay event, import from Mobilizon/Transiscope |
| `update-event` | Modifier evenement | UpdateEventScreen.tsx | Edit existing event |
| `invite` | Inviter des amis | InviteScreen.tsx | Invite contacts to event |
| `participants-list` | Liste des participants | ParticipantsListScreen.tsx | Event participant list |
| `meeting-points` | Points de rencontre | MeetingPointsScreen.tsx | Carpooling/meeting coordination |
### User Module (`src/modules/user/screens/`)
| ID | Name | File | Description |
|----|------|------|-------------|
| `profile` | Mon profil | ProfileScreen.tsx | Current user profile |
| `update-profile` | Modifier mon profil | UpdateProfileScreen.tsx | Edit profile form |
| `user-profile` | Profil d'un utilisateur | UserProfileScreen.tsx | View another user's profile |
| `friends-list` | Mon reseau | FriendsListScreen.tsx | Network/friends list |
| `share-profile` | Partager mon profil | ShareProfileScreen.tsx | QR code + link sharing |
### Auth Module (`src/modules/auth/screens/`)
| ID | Name | File | Description |
|----|------|------|-------------|
| `login` | Connexion | LoginScreen.tsx | Login (NextGraph + email fallback) |
## Screen Registry
`src/screens/index.ts` imports all screens and exports:
```typescript
export interface ScreenProps {
navigate: (screenId: string) => void;
}
export const screenGroups: ScreenGroup[] // Grouped: home, events, user, general
export const screens: Screen[] // Flat list
export function getScreen(id: string): Screen | undefined
```
## Sketchy Component Library
`src/shared/components/sketchy/` — hand-drawn UI with custom font:
| Component | Usage |
|-----------|-------|
| `Header` | Screen header with back button |
| `NavBar` | Bottom tab navigation |
| `Button` | Action buttons |
| `Card` | Content cards |
| `Input` | Text inputs |
| `Title`, `Subtitle`, `Text` | Typography |
| `Avatar` | User avatars with initials |
| `Badge` | Status/category badges |
| `Toggle`, `Checkbox` | Form controls |
| `ListItem` | List row items |
| `Divider` | Section separators |
| `Placeholder` | Image/content placeholders |
| `PhoneFrame` | Phone device frame wrapper |
| `BrokerBanner` | NextGraph connection status banner |
| `NgStatus` | Connection indicator dot |
## Screen Patterns
All screens follow the same pattern:
```typescript
import { Header, Button, ... } from '../../../shared/components/sketchy';
import { useFestipodData } from '../../../shared/context/FestipodDataContext';
import type { ScreenProps } from '../../../screens';
export function MyScreen({ navigate }: ScreenProps) {
const { events, currentUser, ... } = useFestipodData();
// render with sketchy components
}
```
Navigation between screens uses `navigate(screenId)` — the prototyping tool intercepts this to switch the displayed screen.
@@ -1,90 +0,0 @@
# Test Layer Contracts
Each BDD test layer (`@ui`, `@data`, `@e2e`) answers a distinct question. Mixing concerns produces brittle tests that fail on refactors without catching real regressions.
## Overview
```
/\ @e2e ~10 scénarios, parcours utilisateur critiques
/ \
/----\
/ @data\ ~10 scénarios, mutations & persistance NG
/--------\
/ @ui \ ~60 scénarios, 1-5 par état d'écran × 15 écrans
/____________\
```
The pyramid reflects cost: `@ui` runs in-process (instant), `@data` boots a broker (~50s for the suite), `@e2e` boots broker + real app + navigates a real browser (~2min). Move every assertion to the lowest layer that can answer the question — UI rendering claims belong in `@ui`, not `@e2e`.
## Key Concepts
- **`@ui` — display layer.** Renders a screen with `LocalDataProvider` (seed data) + happy-dom and asserts on the resulting DOM. Verifies that *given known data, the screen shows the expected text and elements*. Does **not** test navigation outcomes, mutations, or data persistence.
- **`@data` — data layer.** Drives ORM mutations through the real NextGraph broker via a headless test harness. No app UI involved. Verifies that *operations on shapes are correctly persisted and observable in the wallet*. See [data-layer-testing](./data-layer-testing.md).
- **`@e2e` — integration layer.** Boots the real app inside the broker iframe with a Playwright-controlled Chromium. Verifies that *layers collaborate to deliver a user journey* (e.g. create → list → modify → reload → still there). Sparse: 1 scenario per critical path; never duplicate `@ui` content checks here.
## Implementation
### `@ui` — rendering helper
`src/shared/test-harness/renderHelper.tsx` installs happy-dom globals and renders any screen wrapped in `LocalDataProvider` + `RouterProvider`. Called from `world.ts:renderCurrentScreen()` on every `navigateTo(...)`. Seed data (`src/shared/data/seedData.ts`) provides predictable fixtures — `Marie Dupont`/`@mariedupont` is `currentUser`, `Jean Durand`/`@jeandurand` exists in `users`, 5 seed events, etc.
**Good `@ui` assertion patterns:**
```ts
// Text visible to the user
expect(this.getDomText()).to.include('Marie Dupont');
// Element presence by class/role
expect(this.renderedDoc!.querySelector('.app-avatar')).to.not.be.null;
// Conditional rendering (filled state vs empty state)
const cards = this.renderedDoc!.querySelectorAll('.app-card');
expect(cards.length).to.be.greaterThan(0);
// Required form fields rendered with their label + asterisk
const labels = Array.from(this.renderedDoc!.querySelectorAll('p'))
.map(p => p.textContent ?? '');
expect(labels.some(t => t.includes("Nom de l'événement *"))).to.be.true;
```
**Anti-patterns to remove:**
```ts
// ❌ Regex on source: couples test to code structure, fails on refactor
expect(/<Title[^>]*>Marie Dupont<\/Title>/.test(source)).to.be.true;
// ❌ Testing implementation details
expect(/showDuplicateWarning/.test(source)).to.be.true;
expect(/importableEvents/.test(source)).to.be.true;
// ❌ Testing JSX structure rather than rendered output
expect(/<Avatar[^>]*initials="MD"[^>]*size="lg"/.test(source)).to.be.true;
```
### `@data` — broker-only
Already isolated correctly. See [data-layer-testing](./data-layer-testing.md). Don't touch the DOM here; use the test harness bridge (`window.__testData`).
### `@e2e` — full stack
Path-based routing: navigate via `window.history.pushState` + `popstate` dispatch (`src/modules/auth/steps/e2e/connexion.steps.ts`). Assert on actual DOM text after `appFrame.waitForFunction`. **Do not** re-verify here what `@ui` already covers — `@e2e` should fail when *collaboration* between layers breaks, not when an icon changes.
## Migration Consequences
The current `@ui` suite predates this contract. The migration plan:
1. **Rewrite source-grep assertions** → DOM queries via the helper. The `world.ts:hasText/hasField/hasElement` methods already prefer the rendered DOM and fall back to source — so unmigrated steps still work during the transition.
2. **Delete tests on implementation details** (`/showDuplicateWarning/`, `/importableEvents/`, regex on JSX). They protect nothing the user sees.
3. **Move behavioral assertions to `@e2e`** when not already covered ("clicking Suivant advances the wizard" — exercise it via Playwright if it's not redundant with existing journeys).
4. **Drop redundant `@e2e` content checks** that duplicate `@ui` (e.g. "screen contains 'Découvrir'" — let `@ui` own that).
`world.ts:screenFileMap`, `screenFieldDetectors`, `screenExpectedContent`, `screenRequiredFields` are vestiges of the source-analysis era. Once the migration is complete, they can be removed in favor of seed-data assertions on the rendered DOM.
## See Also
- [BDD Testing setup](./bdd-testing.md) — Cucumber config, file layout, scripts
- [Data Layer](./data-layer.md) — NextGraph shapes, seed data, contexts
- [Data-Layer Testing](./data-layer-testing.md) — broker harness, wallet setup, Playwright
- [Architecture](./architecture.md) — module structure
+23 -67
View File
@@ -1,75 +1,31 @@
# Festipod
Mobile-first web app for discovering and sharing festival/event recommendations through trusted networks.
Web app mobile-first où les utilisateurs créent des **points de rencontre** qui se *greffent* sur des **événements publics** existants, pour favoriser les rencontres. L'événement n'est qu'un prétexte/ancrage ; la valeur, c'est le point de rencontre — **on s'inscrit à un point de rencontre, pas à un événement**. Stack : Bun + React + NextGraph (P2P, local-first, chiffré).
## Architecture
## Invariants à toujours garder
Feature-based: code organized by business domain, not technical layer. See [architecture](.project/knowledge/architecture.md).
- **Architecture feature-based** : le code est organisé par domaine métier, pas par couche technique.
```
src/modules/{event,user,home,auth,workshop,meeting,notification}/
src/shared/ # Composants, context, data — importable par tous les modules
src/app/ # App shell (router, providers, entrée)
src/screens/index.ts # Registre d'écrans (utilisé par Storybook)
```
- **Un module n'importe QUE depuis `shared/` — jamais d'un autre module.** C'est l'invariant qui rend l'archi réelle.
- **Bun-first** : `bun` / `bun install` / `bun test` / `bun build`, jamais node/npm/vite/jest. `bun run dev` (port 3000).
```
src/modules/{event,user,home,auth,workshop,meeting,notification}/
src/shared/ # Components, context, data — importable by all modules
src/app/ # App shell (router, providers, entry point)
src/screens/index.ts # Screen registry (used by Storybook)
```
## Doctrine du projet — concepts (livrée automatiquement)
## Routing
La connaissance détaillée vit dans `.project/concepts/` (système *concept*) : fiches courtes, typées, **livrées par un hook quand tu touches leur territoire** — tu n'as pas à les charger d'avance. Les 6 concepts :
Path-based routing with History API (custom router in `src/app/router.tsx`).
| Concept | Couvre |
|---|---|
| `functional-domain` | Modèle produit : point de rencontre, acteurs, concepts métier, défi déduplication |
| `app-architecture` | Modules, invariant d'imports, app shell, routing path-based, écrans |
| `tech-stack` | Bun-first, APIs Bun, build pipeline, commandes |
| `data-layer` | NextGraph actuel (mono-store), shapes, modes connected/demo, règles + pièges (suppression, champs perdus, internals) |
| `bdd-testing` | Cucumber multi-couches FR, contrat `@ui`/`@data`/`@e2e`, harness broker, cookbook |
| `app-security` | Posture de sécurité actuelle (mono-store, confiance broker), auth wallet, modèle d'autorisations cible |
| `nextgraph-platform` | NextGraph système externe (stores, inbox, SDK) + briefs prospectifs (multi-store, fork, shim) |
| Path | Screen |
|------|--------|
| `/` | WelcomeScreen |
| `/login` | LoginScreen |
| `/home` | HomeScreen |
| `/events` | EventsScreen |
| `/events/new` | CreateEventScreen |
| `/events/:id` | EventDetailScreen |
| `/events/:id/edit` | UpdateEventScreen |
| `/events/:id/invite` | InviteScreen |
| `/events/:id/participants` | ParticipantsListScreen |
| `/events/:id/meeting-points` | MeetingPointsScreen |
| `/profile` | ProfileScreen |
| `/profile/edit` | UpdateProfileScreen |
| `/profile/friends` | FriendsListScreen |
| `/profile/share` | ShareProfileScreen |
| `/users/:id` | UserProfileScreen |
| `/settings` | SettingsScreen |
Screens use `useNavigate()` and `useParams()` hooks from the router — no prop drilling.
## Data Layer
NextGraph (P2P/local-first) with SHEX shapes and ORM. See [data-layer](.project/knowledge/data-layer.md).
## BDD Testing
Multi-layer Cucumber/Gherkin in French. See [bdd-testing](.project/knowledge/bdd-testing.md) for the setup and [test-layer-contracts](.project/knowledge/test-layer-contracts.md) for what each layer is allowed to test.
`@ui` scenarios render screens in-process (happy-dom + seed data) and assert on the DOM. `@data` scenarios test data operations through the real NextGraph broker. `@e2e` scenarios test the real app UI in the broker iframe. See [data-layer-testing](.project/knowledge/data-layer-testing.md).
## Quick Start
```bash
bun run dev # Dev server with HMR (port 3000)
bun run build # Production build to dist/
bun run storybook # Browse screens and components
bun run test:cucumber # Run all BDD tests
bun run features:parse # Regenerate features.ts from .feature files
bun run steps:extract # Extract step definitions for tooltips
bun run build:orm # Regenerate ORM from SHEX shapes
```
## Documentation
- [Architecture](.project/knowledge/architecture.md) — module structure, import rules, app shell
- [Data Layer](.project/knowledge/data-layer.md) — NextGraph, shapes, context, seed data
- [BDD Testing](.project/knowledge/bdd-testing.md) — Cucumber setup, step layers, feature files
- [Test Layer Contracts](.project/knowledge/test-layer-contracts.md) — what each of `@ui`/`@data`/`@e2e` is allowed to test
- [Screens](.project/knowledge/screens.md) — screen inventory, registry, sketchy components
- [Data-Layer Testing](.project/knowledge/data-layer-testing.md) — real broker testing, wallet setup, Playwright harness, e2e layer
## Briefs (work not yet started)
- [Multi-store refactor](.project/briefs/multi-store-refactor.md) — passer du mono-store actuel à une structure de Group stores par communauté/event/RDV (prérequis multi-user)
- [Matrice d'autorisations et requêtes](.project/briefs/authorization-matrix.md) — analyse qui doit guider la structure de stores cible
Pour **documenter** un fait projet : `/concept document <sujet>` (ne pas écrire en libre dans `.project/`).
+4 -191
View File
@@ -2,195 +2,8 @@
# Festipod Project
Mobile-first web app for discovering and sharing festival/event recommendations through trusted networks. Uses a sketchy hand-drawn UI style.
Le cœur toujours-chargé (but produit, invariants, conventions Bun-first, carte des concepts) vit dans `@AGENTS.md` ci-dessus. Toute la doctrine détaillée est dans `.project/concepts/` et **livrée automatiquement par le hook concept** quand tu touches le territoire concerné — ne la recopie pas ici.
## Architecture
Feature-based architecture: code is organized by business domain (module), not by technical layer. A module can only import from `shared/` — never from another module.
Multi-layer BDD: each module has `steps/ui/`, `steps/data/`, `steps/e2e/` directories. Shared step definitions live in `src/shared/steps/`.
## Project Structure
```
src/
modules/ # Business domain modules
event/ # Events (create, discover, detail, update, invite, participants, meeting points)
screens/ # EventsScreen, EventDetailScreen, CreateEventScreen, etc.
features/ # Gherkin .feature files for this domain
steps/ # BDD step definitions
ui/ # UI-layer steps
data/ # Data-layer steps
e2e/ # E2E steps
user/ # User profiles, friends, sharing
screens/ # ProfileScreen, FriendsListScreen, ShareProfileScreen, etc.
features/
steps/
home/ # Home dashboard, settings
screens/ # HomeScreen, SettingsScreen
auth/ # Authentication, onboarding
screens/ # LoginScreen, WelcomeScreen
workshop/ # Workshop/atelier specs (no screens yet)
features/
steps/
meeting/ # Meeting point specs
features/
steps/
notification/ # Notification specs
features/
steps/
shared/ # Shared code (importable by all modules)
components/
sketchy/ # Hand-drawn UI components (Button, Card, Avatar, etc.)
ui/ # Shadcn/Radix components
context/ # ThemeContext, NextGraphContext, FestipodDataContext
data/ # User stories, features.ts (auto-generated), testResults.ts
hooks/ # Custom hooks (useShapeWithDefaults)
shapes/ # SHEX shapes + ORM bindings (NextGraph)
utils/ # ngSession, ngBootstrap
steps/ # Shared BDD step definitions (cross-domain)
ui/ # navigation.steps.ts, form.steps.ts, screen.steps.ts
data/
support/ # Cucumber hooks.ts, world.ts
types/ # TypeScript type definitions
lib/ # Utility functions (cn, etc.)
app/ # App shell
App.tsx # Root component with providers + route switch
router.tsx # Path-based routing (History API)
frontend.tsx # React entry point
screens/
index.ts # Screen registry (used by Storybook)
scripts/ # Build scripts for parsing features
docs/ # Documentation
.storybook/ # Storybook configuration
```
## Key Commands
```bash
bun run dev # Start dev server with HMR
bun run storybook # Browse screens and components in Storybook
bun run test:cucumber # Run Cucumber tests
bun run features:parse # Regenerate features.ts from .feature files
bun run steps:extract # Extract step definitions for tooltips
```
## Routing
Path-based routing via `src/app/router.tsx`. Screens use `useNavigate()` and `useParams()` hooks. See AGENTS.md for the full route table.
## Conventions
- Gherkin specs are in French (Etant donne, Quand, Alors)
- UI labels are in French
- User stories are prefixed US-1 to US-26
- Screens use the sketchy component library, not Tailwind
- Max app width: 768px (tablet portrait)
---
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
import { createRoot } from "react-dom/client";
// import .css files directly and it works
import './index.css';
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
- Specs Gherkin et libellés UI en **français** (`Etant donné`, `Quand`, `Alors`).
- Conventions techniques (Bun, APIs, build) : concept `tech-stack`. Architecture et écrans : concept `app-architecture`.
- Documenter un fait projet : `/concept document <sujet>`.
+4 -6
View File
@@ -40,7 +40,7 @@ Implémentées dans le code (écrans visibles via le router) :
- Liste d'amis (connexions)
- Profil d'un autre utilisateur
Voir le tableau des routes dans [AGENTS.md](./AGENTS.md#routing) et l'inventaire des écrans dans [.project/knowledge/screens.md](./.project/knowledge/screens.md).
Voir l'inventaire des routes et des écrans dans le concept [app-architecture](./.project/concepts/app-architecture/).
### Défis ouverts
@@ -56,7 +56,7 @@ Identifiées comme nécessaires (notamment pour la scalabilité et la découvert
- **Abonnement à une communauté d'intérêt** pour découvrir ses événements (mécanisme de discovery distribué).
- **Abonnement à un utilisateur** pour suivre les événements qu'il déclare (sans nécessairement être ami).
- **Listes curated** — créer et partager des sélections d'événements éditorialisées.
- **Multi-utilisateurs collaboratif** : aujourd'hui chaque utilisateur a ses données isolées dans son wallet. Le passage en mode collaboratif (un point de rencontre vu par plusieurs personnes) suppose un refactor de la couche données vers les Group stores NextGraph. Voir [brief multi-store-refactor](./.project/briefs/multi-store-refactor.md).
- **Multi-utilisateurs collaboratif** : aujourd'hui chaque utilisateur a ses données isolées dans son wallet. Le passage en mode collaboratif (un point de rencontre vu par plusieurs personnes) suppose un refactor de la couche données. Voir le concept [nextgraph-platform](./.project/concepts/nextgraph-platform/) (briefs multi-store, matrice d'autorisations, wallet partagé, fork inbox).
## Quick Start
@@ -78,7 +78,5 @@ bun run build:orm # Régénérer l'ORM depuis les SHEX shapes
## Documentation
- [AGENTS.md](./AGENTS.md) — architecture, routes, points d'entrée pour contribuer
- [.project/knowledge/](./.project/knowledge/) — comment les choses fonctionnent (data layer, BDD, écrans…)
- [.project/decisions/](./.project/decisions/) — choix techniques figés
- [.project/briefs/](./.project/briefs/) — chantiers à venir, recherche préparatoire
- [AGENTS.md](./AGENTS.md) — cœur : but produit, invariants, carte des concepts
- [.project/concepts/](./.project/concepts/) — toute la doctrine projet (savoir, règles, décisions, briefs), typée et livrée par hook au moment pertinent. 6 concepts : `functional-domain`, `app-architecture`, `tech-stack`, `data-layer`, `bdd-testing`, `nextgraph-platform`.
+22
View File
@@ -133,10 +133,32 @@ const result = await Bun.build({
sourcemap: "linked",
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
// Access gate (ON by default) + shared wallet password, baked into the
// browser bundle as globals (see src/app/AuthGate.tsx, sharedWallet.ts). The
// wallet FILE is copied into the outdir below (served at /shared-wallet.ngw).
"globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__": JSON.stringify(
process.env.ACCESS_GATE_DISABLED === "1",
),
"globalThis.__FESTIPOD_SHARED_WALLET_PASSWORD__": JSON.stringify(
process.env.FESTIPOD_SHARED_WALLET_PASSWORD ?? "",
),
// The app may bootstrap demo data into an empty connected wallet only when
// FESTIPOD_AUTO_SEED is set (see src/shared/utils/autoSeed.ts). Absent → OFF.
"globalThis.__FESTIPOD_AUTO_SEED__": JSON.stringify(
process.env.FESTIPOD_AUTO_SEED ?? "",
),
},
...cliConfig,
});
// Staging: copy the shared wallet FILE into the bundle so the access gate can
// offer it for download (served at /shared-wallet.ngw). See sharedWallet.ts.
if (process.env.FESTIPOD_SHARED_WALLET_FILE) {
const { copyFileSync } = await import("fs");
copyFileSync(process.env.FESTIPOD_SHARED_WALLET_FILE, path.join(outdir, "shared-wallet.ngw"));
console.log(`📦 Copied shared wallet → ${path.join(outdir, "shared-wallet.ngw")}`);
}
const end = performance.now();
const outputTable = result.outputs.map(output => ({
-1380
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -12,6 +12,7 @@
"html:reports/cucumber-report.html"
],
"language": "fr",
"tags": "not @wip",
"formatOptions": {
"snippetInterface": "async-await"
},
+22 -15
View File
@@ -8,26 +8,27 @@
"start": "NODE_ENV=production bun src/index.ts",
"build": "bun run build.ts",
"test:cucumber": "bun run cucumber:run && bun run cucumber:report && bun run features:parse && bun run steps:extract",
"cucumber:run": "node --import tsx/esm node_modules/.bin/cucumber-js --config cucumber.json",
"test:data": "node --import tsx/esm node_modules/.bin/cucumber-js --config cucumber.json --tags @data",
"cucumber:run": "node --import tsx/esm node_modules/@cucumber/cucumber/bin/cucumber.js --config cucumber.json",
"test:data": "node --import tsx/esm node_modules/@cucumber/cucumber/bin/cucumber.js --config cucumber.json --tags @data",
"test:auth-setup": "bun scripts/setup-test-auth.ts",
"cucumber:report": "bun scripts/parse-test-results.ts",
"features:parse": "bun scripts/parse-features.ts",
"steps:extract": "bun scripts/extract-step-definitions.ts",
"build:orm": "rdf-orm build --input ./src/shapes/shex --output ./src/shapes/orm",
"build:orm": "rdf-orm build --input ./src/shared/shapes/shex --output ./src/shared/shapes/orm",
"build:ng": "bash scripts/build-ng-packages.sh",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@ng-org/alien-deepsignals": "0.1.2-alpha.11",
"@ng-org/orm": "0.1.2-alpha.18",
"@ng-org/shex-orm": "0.1.2-alpha.8",
"@ng-org/web": "0.1.2-alpha.13",
"@ng-org/alien-deepsignals": "0.1.2-alpha.13",
"@ng-org/orm": "0.1.2-alpha.21",
"@ng-org/shex-orm": "0.1.2-alpha.9",
"@ng-org/web": "0.1.2-alpha.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@refinedev/core": "^5.0.12",
"bun-plugin-tailwind": "^0.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -40,6 +41,11 @@
"@cucumber/cucumber": "^12.5.0",
"@cucumber/gherkin": "^29.0.0",
"@cucumber/messages": "^26.0.1",
"@storybook/addon-a11y": "^10.3.5",
"@storybook/addon-docs": "^10.3.5",
"@storybook/addon-onboarding": "^10.3.5",
"@storybook/addon-webpack5-compiler-swc": "^4.0.3",
"@storybook/react-webpack5": "^10.3.5",
"@types/bun": "latest",
"@types/chai": "^5.2.3",
"@types/react": "^19",
@@ -47,14 +53,15 @@
"chai": "^6.2.2",
"happy-dom": "^16.6.0",
"playwright": "^1.58.2",
"storybook": "^10.3.5",
"tailwindcss": "^4.1.11",
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0",
"storybook": "^10.3.5",
"@storybook/react-webpack5": "^10.3.5",
"@storybook/addon-webpack5-compiler-swc": "^4.0.3",
"@storybook/addon-a11y": "^10.3.5",
"@storybook/addon-docs": "^10.3.5",
"@storybook/addon-onboarding": "^10.3.5"
}
"tw-animate-css": "^1.4.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"bun"
]
},
"packageManager": "pnpm@10.26.0+sha512.3b3f6c725ebe712506c0ab1ad4133cf86b1f4b687effce62a9b38b4d72e3954242e643190fc51fa1642949c735f403debd44f5cb0edd657abe63a8b6a7e1e402"
}
+6145
View File
File diff suppressed because it is too large Load Diff
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env bun
/**
* overlay-local-checkout.ts — Reactive local overlay of a data-layer PROVIDER's checkout
* into node_modules. One script, one provider per run; only the paths differ between them.
*
* PROVIDERS (first non-flag argument; defaults to `polyfill`):
* polyfill → node_modules/@ng-eventually/polyfill override: NG_EVENTUALLY_LOCAL
* indexing → node_modules/@ng-helpers/indexing override: NG_HELPERS_LOCAL
*
* WHY a copy-overlay and not a symlink (identical for every provider):
* A committed prod dependency installs from Gitea (git+https) into pnpm's store WITHOUT
* its own node_modules/@ng-org, so @ng-org/web resolves UP to Festipod → ONE @ng-org
* instance (one verifier). A local CHECKOUT, however, carries its own node_modules/*
* (links into that provider's own dev tree). Symlinking node_modules/<pkg> to the
* checkout would put the checkout's copies in the resolution path → a SECOND @ng-org
* (and, for `indexing`, a second @ng-eventually/polyfill) → broken SDK, two verifiers.
* So we overlay a real directory containing ONLY the provider's source (no node_modules):
* shared packages still resolve up to Festipod, single instance preserved.
*
* WHAT IT DOES:
* 1. Replaces node_modules/<package> (the pnpm store symlink) with a real directory
* holding the local checkout's package.json + src (NO node_modules).
* 2. Asserts the single-instance invariant — every package this provider SHARES with
* Festipod must resolve to the same realpath from Festipod and from the overlay —
* and aborts if it would break.
* 3. Watches the local checkout's src and copies each change into the overlay.
*
* ⚠️ RESTART `bun run dev` AFTER THIS SCRIPT WRITES — a rebuild is NOT a substitute.
* A running dev server NEVER picks up a package refreshed inside node_modules, not even
* across a genuine rebuild: VERIFIED in a controlled test, an application-source edit
* produced a new bundle hash and the rebuilt bundle STILL carried the stale dependency.
* The server's resolution of that import is pinned at process start and a rebuild does not
* re-resolve it. Only restarting serves the fresh copy, and nothing warns you — a stale
* server looks exactly like a current one. See
* .project/concepts/tech-stack/caveat_polyfill-overlay-needs-a-dev-restart.md, which cost
* an hour to learn. Watching copies the files; it does not make anything reload them.
*
* USAGE (reactive dev):
* Terminal 1: pnpm run overlay:polyfill # or: pnpm run overlay:indexing
* Terminal 2: bun run dev # portless festipod bun --hot src/index.ts
* Edit the checkout's src → it lands in node_modules → RESTART dev to pick it up.
*
* pnpm run overlay:indexing --once # overlay + verify, no watch (CI / one-shot)
* Return to the committed git-installed dependencies: pnpm install
*/
import { existsSync, lstatSync, mkdirSync, rmSync, cpSync, copyFileSync, realpathSync } from "node:fs";
import { watch } from "node:fs";
import { join, dirname } from "node:path";
interface Provider {
/** Package as installed, e.g. "@ng-eventually/polyfill" — also its node_modules path. */
readonly packageName: string;
/** Local checkout used when the env override is unset. */
readonly defaultLocal: string;
/** Env var overriding the local checkout path. */
readonly envOverride: string;
/**
* Packages this provider SHARES with Festipod and that must stay single-instance.
* Each is resolved from Festipod and from the overlay; the realpaths must match.
*/
readonly singletons: readonly string[];
}
const PROVIDERS: Record<string, Provider> = {
polyfill: {
packageName: "@ng-eventually/polyfill",
defaultLocal: "/home/sylvain/projects/nextgraph/ng-eventually-js/packages/polyfill",
envOverride: "NG_EVENTUALLY_LOCAL",
singletons: ["@ng-org/web"],
},
indexing: {
packageName: "@ng-helpers/indexing",
defaultLocal: "/home/sylvain/projects/nextgraph/ng-helpers",
envOverride: "NG_HELPERS_LOCAL",
// Consumes the polyfill, so BOTH it and the verifier underneath must stay single.
singletons: ["@ng-eventually/polyfill", "@ng-org/web"],
},
};
const FESTIPOD = realpathSync(join(import.meta.dir, ".."));
const args = process.argv.slice(2);
const ONCE = args.includes("--once");
const KEY = args.find((a) => !a.startsWith("-")) ?? "polyfill";
function fail(msg: string): never {
console.error(`✖ overlay:${KEY} — ${msg}`);
process.exit(1);
}
const provider = PROVIDERS[KEY];
if (!provider) {
fail(`unknown provider "${KEY}" — expected one of: ${Object.keys(PROVIDERS).join(", ")}`);
}
const LOCAL = process.env[provider.envOverride] ?? provider.defaultLocal;
const TARGET = join(FESTIPOD, "node_modules", ...provider.packageName.split("/"));
const SRC_LOCAL = join(LOCAL, "src");
const SRC_TARGET = join(TARGET, "src");
if (!existsSync(join(LOCAL, "package.json"))) {
fail(`local checkout not found at ${LOCAL} (set ${provider.envOverride} to override)`);
}
if (!existsSync(SRC_LOCAL)) {
fail(`local checkout has no src/ at ${SRC_LOCAL}`);
}
// 1. Replace the pnpm store symlink with a real overlay dir (metadata + src, NO node_modules).
console.log(`→ overlaying local ${provider.packageName}: ${LOCAL}`);
if (existsSync(TARGET) || lstatSync(TARGET, { throwIfNoEntry: false })) {
rmSync(TARGET, { recursive: true, force: true });
}
mkdirSync(TARGET, { recursive: true });
for (const meta of ["package.json", "tsconfig.json", "README.md"]) {
const from = join(LOCAL, meta);
if (existsSync(from)) copyFileSync(from, join(TARGET, meta));
}
// Copy src fresh (NEVER a node_modules dir — that is what guarantees single instances).
cpSync(SRC_LOCAL, SRC_TARGET, { recursive: true });
// 2. Assert the single-instance invariant for every package shared with Festipod.
const overlayReal = realpathSync(TARGET);
for (const spec of provider.singletons) {
const fromFestipod = realpathSync(Bun.resolveSync(spec, FESTIPOD));
const fromOverlay = realpathSync(Bun.resolveSync(spec, overlayReal));
console.log(` ${spec} (Festipod): ${fromFestipod}`);
console.log(` ${spec} (overlay) : ${fromOverlay}`);
if (fromFestipod !== fromOverlay) {
fail(
`single-instance invariant BROKEN — ${spec} resolves to two different realpaths.\n` +
" The overlay must not contain its own node_modules. Aborting.",
);
}
}
console.log(`✓ single instance preserved for: ${provider.singletons.join(", ")}`);
if (ONCE) {
console.log("✓ overlay ready (--once, not watching)");
process.exit(0);
}
// 3. Watch and copy on change. This keeps the overlay CURRENT; it does NOT make a running
// dev server notice — not even across a rebuild (see the header). Restart it.
console.log(`👀 watching ${SRC_LOCAL} → ${SRC_TARGET} (Ctrl-C to stop)`);
watch(SRC_LOCAL, { recursive: true }, (_event, filename) => {
if (!filename) return;
const from = join(SRC_LOCAL, filename);
const to = join(SRC_TARGET, filename);
try {
if (existsSync(from)) {
mkdirSync(dirname(to), { recursive: true });
copyFileSync(from, to);
console.log(` ↻ ${filename}`);
} else if (existsSync(to)) {
rmSync(to, { force: true });
console.log(` ✗ ${filename} (removed)`);
}
} catch (err) {
console.error(` ! failed to sync ${filename}:`, err);
}
});
+19 -10
View File
@@ -1,12 +1,14 @@
import { RouterProvider, useRouter } from './router';
import { ThemeProvider } from '../shared/context/ThemeContext';
import { NextGraphProvider } from '../shared/context/NextGraphContext';
import { AccountProvider } from '../shared/context/AccountContext';
import { RefineProvider } from '../shared/refine/RefineProvider';
import { FestipodDataProvider } from '../shared/context/FestipodDataContext';
import { AuthGate } from './AuthGate';
import { ToastContainer } from '../shared/components/sketchy';
// Auth
import { WelcomeScreen } from '../modules/auth/screens/WelcomeScreen';
import { LoginScreen } from '../modules/auth/screens/LoginScreen';
// Home
import { HomeScreen } from '../modules/home/screens/HomeScreen';
@@ -34,7 +36,6 @@ function AppContent() {
switch (route.page) {
case 'welcome': return <WelcomeScreen />;
case 'login': return <LoginScreen />;
case 'home': return <HomeScreen />;
case 'events': return <EventsScreen />;
case 'create-event': return <CreateEventScreen />;
@@ -57,14 +58,22 @@ export function App() {
return (
<ThemeProvider>
<NextGraphProvider>
<FestipodDataProvider>
<RouterProvider>
<div className="app-container">
<AppContent />
<ToastContainer />
</div>
</RouterProvider>
</FestipodDataProvider>
<AccountProvider>
{/* Refine wraps the data context: FestipodDataContext reads and writes
events through Refine's hooks, so it must sit inside <Refine>. */}
<RefineProvider>
<FestipodDataProvider>
<RouterProvider>
<div className="app-container">
<AuthGate>
<AppContent />
</AuthGate>
<ToastContainer />
</div>
</RouterProvider>
</FestipodDataProvider>
</RefineProvider>
</AccountProvider>
</NextGraphProvider>
</ThemeProvider>
);
+48
View File
@@ -0,0 +1,48 @@
/**
* AuthGate — the stopgap access flow (see decision_2026-06-15_shared-wallet-login-flow):
* 1. Technical access barrier (AccessGateScreen) → opens the SHARED wallet via
* the broker redirect (with the wallet file + guide it hands the user).
* 2. Perceived app login (ConnexionScreen) → pick a username.
* 3. The app.
*
* The gate is ON BY DEFAULT (Festipod never functions without NextGraph). It is
* disabled only when `globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ === true` —
* injected by `build.ts` (from ACCESS_GATE_DISABLED=1) for a no-gate build, or
* by the test harness via `context.addInitScript` for @e2e (which exercises the
* screens, not the auth flow). Absent → gate ON.
*/
import type { ReactNode } from 'react';
import { useNextGraph } from '../shared/context/NextGraphContext';
import { useAccount } from '../shared/context/AccountContext';
import { AccessGateScreen } from '../modules/auth/screens/AccessGateScreen';
import { ConnexionScreen } from '../modules/auth/screens/ConnexionScreen';
declare global {
// eslint-disable-next-line no-var
var __FESTIPOD_ACCESS_GATE_DISABLED__: boolean | undefined;
}
const GATE_DISABLED = globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ === true;
export function AuthGate({ children }: { children: ReactNode }) {
const { status, error, connect } = useNextGraph();
const { username } = useAccount();
// Gate explicitly disabled (no-gate build / @e2e harness) → straight to app.
if (GATE_DISABLED) {
return <>{children}</>;
}
// 1. Technical access barrier (real NG login) — until the shared wallet opens.
if (status !== 'connected') {
return <AccessGateScreen status={status} error={error} onEnter={connect} />;
}
// 2. Perceived app login — until a username is chosen.
if (!username) {
return <ConnexionScreen />;
}
// 3. The app.
return <>{children}</>;
}
-3
View File
@@ -6,7 +6,6 @@ import React, { createContext, useContext, useState, useEffect, useCallback } fr
type Route =
| { page: 'welcome' }
| { page: 'login' }
| { page: 'home' }
| { page: 'events' }
| { page: 'create-event' }
@@ -38,7 +37,6 @@ function parsePath(pathname: string): Route {
const path = pathname.replace(/\/+$/, '') || '/';
if (path === '/' || path === '') return { page: 'welcome' };
if (path === '/login') return { page: 'login' };
if (path === '/home') return { page: 'home' };
if (path === '/events') return { page: 'events' };
if (path === '/events/new') return { page: 'create-event' };
@@ -73,7 +71,6 @@ function parsePath(pathname: string): Route {
export function routeToPath(route: Route): string {
switch (route.page) {
case 'welcome': return '/';
case 'login': return '/login';
case 'home': return '/home';
case 'events': return '/events';
case 'create-event': return '/events/new';
@@ -6,29 +6,10 @@ Fonctionnalité: Connexion NextGraph et chargement des données
Et charger les données de test dans mon portefeuille
Afin d'utiliser l'application avec mes propres données
# --- UI layer: écran de connexion ---
@ui
Scénario: L'écran de connexion affiche le bouton NextGraph
Étant donné je suis sur la page "connexion"
Alors l'écran contient un bouton "Se connecter avec NextGraph"
@ui @wip
# Behavioral: requires simulating an NG status change. Better tested at the
# @e2e layer where a real connected session triggers the redirect.
Scénario: L'écran de connexion redirige automatiquement quand connecté
Étant donné je suis sur la page "connexion"
Alors l'écran gère la redirection automatique après connexion
@ui
Scénario: L'état initial est "en cours" quand une connexion est en attente
Étant donné je suis sur la page "connexion"
Alors l'écran gère l'état de connexion en cours
@ui
Scénario: Aucune donnée de démonstration n'est visible pendant la connexion
Étant donné je suis sur la page "connexion"
Alors l'écran n'importe pas de données de démonstration
# NB : l'ancien écran /login (LoginScreen) a été retiré — l'accès NextGraph
# passe désormais par l'AccessGateScreen (barrière ON par défaut), cf.
# decision_2026-06-17_assisted-wallet-import. Les scénarios @ui qui testaient
# le LoginScreen ont été supprimés en conséquence.
# --- Data layer: comportement du portefeuille ---
@@ -59,11 +40,6 @@ Fonctionnalité: Connexion NextGraph et chargement des données
# --- E2E layer: comportement réel dans le navigateur ---
@e2e
Scénario: L'écran de connexion redirige vers l'accueil si déjà connecté
Quand l'utilisateur navigue vers l'écran "login"
Alors l'application affiche l'écran "home"
@e2e
Scénario: La navigation interne met à jour l'URL
Quand l'utilisateur navigue vers l'écran "events"
@@ -0,0 +1,137 @@
/**
* AccessGateScreen — the *technical access barrier* of the stopgap.
*
* STOPGAP (see decision_2026-06-15_shared-wallet-login-flow). This is the
* REAL NextGraph login, shown before the app renders. Because it precedes the
* app, the user reads it as "access to the test environment", not as an app
* login. Clicking "Entrer" triggers `connect()`, which redirects to the broker
* to open the SHARED wallet. After return (inside the broker iframe) NG
* auto-connects and the app shows the perceived login (ConnexionScreen).
*
* ASSISTED IMPORT (see the broker-import constraint, concept nextgraph-platform
* + decision_2026-06-17). The hosted broker can't import a wallet inline during
* web-app auth: a first-time device has no wallet, so the broker redirect would
* dead-end. We therefore HAND the user the shared wallet FILE (download) + the
* shared password and guide a one-time import on nextgraph.eu ("Import a Wallet
* File"), BEFORE they click "Entrer". The wallet FILE is the correct static
* primitive — a TextCode is a transient 5-min transfer, unusable to embed. Shown
* only when a shared wallet is configured (FESTIPOD_SHARED_WALLET_PASSWORD).
*/
import { useState, type ReactNode } from 'react';
import { Button, Title, Text } from '../../../shared/components/sketchy';
import { SHARED_WALLET_PASSWORD, SHARED_WALLET_FILE_URL, WALLET_IMPORT_URL, hasSharedWallet } from '../sharedWallet';
interface AccessGateScreenProps {
status: 'disconnected' | 'connecting' | 'connected' | 'error';
error?: string;
onEnter: () => void;
}
// One numbered step: a badge + a title + the action for that step.
function Step({ n, title, children }: { n: number; title: string; children: ReactNode }) {
return (
<div style={{ display: 'flex', gap: 12, marginBottom: 18 }}>
<div style={{
flexShrink: 0, width: 26, height: 26, borderRadius: '50%', background: '#E8590C',
color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700, fontSize: 14,
}}>{n}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text style={{ margin: '2px 0 8px', fontWeight: 600, fontSize: 14 }}>{title}</Text>
{children}
</div>
</div>
);
}
export function AccessGateScreen({ status, error, onEnter }: AccessGateScreenProps) {
const connecting = status === 'connecting';
const [copied, setCopied] = useState(false);
const copyPassword = async () => {
try {
await navigator.clipboard.writeText(SHARED_WALLET_PASSWORD);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// clipboard may be blocked — the password stays selectable
}
};
const entrer = (
<Button
variant="primary"
onClick={onEnter}
disabled={connecting}
style={{ width: '100%', opacity: connecting ? 0.6 : 1 }}
>
{connecting ? 'Accès en cours…' : 'Entrer'}
</Button>
);
return (
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<Title style={{ textAlign: 'center', fontSize: 30, marginBottom: 4 }}>Festipod</Title>
<Text style={{ textAlign: 'center', marginBottom: 24, color: '#888' }}>Espace de test</Text>
{hasSharedWallet() ? (
<>
<Text style={{ textAlign: 'center', fontSize: 14, color: '#666', margin: '0 0 20px', lineHeight: 1.5 }}>
Première connexion sur cet appareil ?<br />Chargez le portefeuille partagé, une seule fois.
</Text>
<Step n={1} title="Téléchargez le portefeuille">
<a
data-testid="shared-wallet-download"
href={SHARED_WALLET_FILE_URL}
download="festipod-wallet.ngw"
style={{
display: 'block', textAlign: 'center', textDecoration: 'none',
padding: 10, borderRadius: 10, background: '#E8590C', color: '#fff', fontWeight: 600, fontSize: 14,
}}
>
⬇ Télécharger le portefeuille
</a>
</Step>
<Step n={2} title="Importez-le sur NextGraph">
<Text style={{ margin: '0 0 8px', fontSize: 13, lineHeight: 1.6, color: '#666' }}>
<a href={WALLET_IMPORT_URL} target="_blank" rel="noopener noreferrer" style={{ color: '#E8590C', fontWeight: 600 }}>
Ouvrir la page d'import
</a>{' '}(nouvel onglet) → « Import a Wallet File » → choisissez le fichier → mot de passe :
</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<code
data-testid="shared-wallet-password"
style={{ flex: 1, padding: '6px 10px', background: '#fff', border: '1px solid #eee', borderRadius: 8, fontSize: 13, userSelect: 'all' }}
>
{SHARED_WALLET_PASSWORD}
</code>
<Button variant="accent-outline" onClick={copyPassword} style={{ padding: '6px 10px', fontSize: 12 }}>
{copied ? 'Copié ✓' : 'Copier'}
</Button>
</div>
</Step>
<Step n={3} title="Revenez sur cet onglet et entrez">
{entrer}
</Step>
</>
) : (
entrer
)}
{status === 'error' && (
<Text style={{ textAlign: 'center', fontSize: 12, color: '#c92a2a', marginTop: 12 }}>
{error || "Accès à l'environnement impossible. Réessayez."}
</Text>
)}
</div>
<Text style={{ textAlign: 'center', fontSize: 12, color: '#bbb' }}>
Version beta
</Text>
</div>
);
}
@@ -0,0 +1,92 @@
/**
* ConnexionScreen — the *perceived* login of the stopgap.
*
* STOPGAP (see decision_2026-06-15_shared-wallet-login-flow). The real NG
* login (AccessGateScreen) already happened and is not perceived as a login;
* THIS screen is what the user experiences as "logging in": they pick a
* username (no password — declarative). The username is persisted by
* AccountContext (localStorage) and resolved against the accounts living in
* the shared wallet.
*/
import { useState } from 'react';
import { Button, Input, Title, Text, Avatar } from '../../../shared/components/sketchy';
import { useAccount } from '../../../shared/context/AccountContext';
import { useFestipodData } from '../../../shared/context/FestipodDataContext';
import { useNavigate } from '../../../app/router';
export function ConnexionScreen() {
const { login } = useAccount();
const { users } = useFestipodData();
const navigate = useNavigate();
const [value, setValue] = useState('');
// Choosing a username completes the login → land on the app (not the '/'
// welcome/onboarding screen, which the access gate has replaced upstream).
const doLogin = (name: string) => {
login(name);
navigate('/home');
};
const submit = () => {
if (value.trim()) doLogin(value);
};
return (
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<Title style={{ textAlign: 'center', fontSize: 32, marginBottom: 8 }}>Connexion</Title>
<Text style={{ textAlign: 'center', marginBottom: 32, color: '#888' }}>
Choisissez votre nom d'utilisateur
</Text>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginBottom: 24 }}>
<Input
placeholder="@votrepseudo"
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === 'Enter') submit();
}}
/>
<Button variant="primary" onClick={submit} disabled={!value.trim()} style={{ width: '100%' }}>
Se connecter
</Button>
</div>
{users.length > 0 && (
<>
<Text style={{ textAlign: 'center', fontSize: 13, color: '#888', margin: '8px 0 12px' }}>
ou reprenez un compte existant
</Text>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{users.map(u => (
<button
key={u.id}
onClick={() => doLogin(u.username)}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: 10,
border: '1px solid #eee',
borderRadius: 12,
background: '#fff',
cursor: 'pointer',
textAlign: 'left',
}}
>
<Avatar initials={u.initials} color="#E8590C" size="sm" />
<span>
<Text style={{ margin: 0, fontWeight: 600 }}>{u.name}</Text>
<Text style={{ margin: 0, fontSize: 12, color: '#888' }}>{u.username}</Text>
</span>
</button>
))}
</div>
</>
)}
</div>
</div>
);
}
@@ -1,14 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { LoginScreen } from './LoginScreen';
import { withProviders } from '../../../../.storybook/decorators';
const meta: Meta<typeof LoginScreen> = {
title: 'Screens/Auth/LoginScreen',
component: LoginScreen,
decorators: [withProviders],
};
export default meta;
type Story = StoryObj<typeof LoginScreen>;
export const Default: Story = {};
-95
View File
@@ -1,95 +0,0 @@
import { useEffect } from 'react';
import { Button, Input, Title, Text, Divider } from '../../../shared/components/sketchy';
import { useNextGraph } from '../../../shared/context/NextGraphContext';
import { useNavigate } from '../../../app/router';
export function LoginScreen() {
const navigate = useNavigate();
const { status, connect } = useNextGraph();
useEffect(() => {
if (status === 'connected') {
navigate('/home');
}
}, [status]);
const handleNgLogin = () => {
if (status === 'connected') {
navigate('/home');
} else {
connect();
}
};
return (
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<Title style={{ textAlign: 'center', fontSize: 32, marginBottom: 8 }}>Festipod</Title>
<Text style={{ textAlign: 'center', marginBottom: 32, color: '#888' }}>Créez et rejoignez des événements entre amis</Text>
{/* NextGraph login */}
<div style={{ marginBottom: 24 }}>
{status === 'connected' ? (
<div style={{ textAlign: 'center', marginBottom: 8 }}>
<Text style={{ color: '#22543D', fontWeight: 'bold', margin: '0 0 8px 0' }}>
✓ Connecté via NextGraph
</Text>
<Button variant="primary" onClick={() => navigate('/home')} style={{ width: '100%' }}>
Continuer vers l'accueil
</Button>
</div>
) : status === 'connecting' ? (
<Button disabled style={{ width: '100%', opacity: 0.6 }}>
Connexion NextGraph en cours...
</Button>
) : (
<div>
<Button
variant="primary"
onClick={handleNgLogin}
style={{ width: '100%' }}
>
Se connecter avec NextGraph
</Button>
{status === 'error' && (
<Text style={{ textAlign: 'center', fontSize: 12, color: '#888', marginTop: 8 }}>
NextGraph non disponible — mode démonstration
</Text>
)}
</div>
)}
</div>
<Divider />
<Text style={{ textAlign: 'center', fontSize: 14, color: '#888', margin: '16px 0' }}>
ou connexion classique (démo)
</Text>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<Text style={{ marginBottom: 4, fontSize: 13, color: '#888' }}>Email</Text>
<Input type="email" placeholder="vous@exemple.com" />
</div>
<div>
<Text style={{ marginBottom: 4, fontSize: 13, color: '#888' }}>Mot de passe</Text>
<Input type="password" placeholder="••••••••" />
</div>
<Button variant="primary" onClick={() => navigate('/home')}>
Se connecter
</Button>
<Text style={{ textAlign: 'center', fontSize: 14, color: '#E8590C' }}>
Mot de passe oublié ?
</Text>
</div>
</div>
<Text style={{ textAlign: 'center', fontSize: 14, color: '#888' }}>
Pas encore de compte ? <span style={{ color: '#E8590C', cursor: 'pointer' }}>S'inscrire</span>
</Text>
</div>
);
}
+12 -2
View File
@@ -1,8 +1,18 @@
import { useEffect } from 'react';
import { Button, Title, Text } from '../../../shared/components/sketchy';
import { useNavigate } from '../../../app/router';
import { useNextGraph } from '../../../shared/context/NextGraphContext';
export function WelcomeScreen() {
const navigate = useNavigate();
const { status } = useNextGraph();
// Onboarding is for NOT-connected users. A connected user landing on '/'
// (e.g. a returning tester past the access gate) goes straight to the app.
useEffect(() => {
if (status === 'connected') navigate('/home');
}, [status]);
return (
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
@@ -41,12 +51,12 @@ export function WelcomeScreen() {
</div>
</div>
<Button variant="primary" onClick={() => navigate('/login')} style={{ marginBottom: 12 }}>
<Button variant="primary" onClick={() => navigate('/home')} style={{ marginBottom: 12 }}>
Rejoindre la communauté
</Button>
<Text style={{ textAlign: 'center', fontSize: 13, color: '#888' }}>
Déjà membre ? <span onClick={() => navigate('/login')} style={{ color: '#E8590C', cursor: 'pointer', fontWeight: 600 }}>Connexion</span>
Déjà membre ? <span onClick={() => navigate('/home')} style={{ color: '#E8590C', cursor: 'pointer', fontWeight: 600 }}>Connexion</span>
</Text>
</div>
+40
View File
@@ -0,0 +1,40 @@
/**
* Shared wallet material for the staging stopgap.
*
* STOPGAP (see brief_2026-06-15_shared-wallet-shim + the broker-import
* constraint, concept nextgraph-platform): the hosted broker can't auto-import
* a wallet, so Festipod HANDS the user the shared wallet and guides a one-time
* import on nextgraph.eu.
*
* The correct primitive is the **wallet FILE** (.ngw), NOT a TextCode: a
* TextCode is a transient device-to-device transfer (5 min, source device
* online, single use) — useless to embed. A wallet file is STATIC and reusable.
* So Festipod serves the file (download) + shows the shared password; the user
* imports it via nextgraph.eu → "Import a Wallet File".
*
* ZERO-SECURITY shared credential (friendly users) → embedding the file +
* password is consistent with the posture.
*
* `build.ts` copies the file (from FESTIPOD_SHARED_WALLET_FILE) to the bundle as
* `/shared-wallet.ngw`, and `define`s the password global from
* FESTIPOD_SHARED_WALLET_PASSWORD. Empty password → no shared wallet configured
* → the gate falls back to the plain flow.
*/
// Build-injected global (not `process.env`, absent in the browser); any path
// that doesn't inject it reads `undefined` → '' safely (no ReferenceError).
declare global {
// eslint-disable-next-line no-var
var __FESTIPOD_SHARED_WALLET_PASSWORD__: string | undefined;
}
export const SHARED_WALLET_PASSWORD: string = globalThis.__FESTIPOD_SHARED_WALLET_PASSWORD__ ?? '';
/** URL of the shared wallet file in the bundle (copied by build.ts). */
export const SHARED_WALLET_FILE_URL = '/shared-wallet.ngw';
/** Standalone NextGraph wallet app — where the import actually happens. */
export const WALLET_IMPORT_URL = 'https://nextgraph.eu/#/wallet/login';
/** Whether Festipod has a shared wallet to hand over (drives the assisted UI). */
export const hasSharedWallet = (): boolean => SHARED_WALLET_PASSWORD.trim().length > 0;
+33 -15
View File
@@ -40,10 +40,13 @@ Given('le portefeuille contient déjà des événements', async function (this:
const td = (window as any).__testData;
td.loadTestData();
});
// Wait for data to propagate
// Wait for data to propagate. `waitForFunction(fn, arg, options)` — the
// timeout goes in the THIRD slot; passed second it is silently taken as the
// predicate's argument and the wait runs on the 30s default instead.
await this.appFrame!.waitForFunction(
() => (window as any).__testData.events.size > 0,
{ timeout: 10000 },
undefined,
{ timeout: 75000 },
);
}
});
@@ -58,22 +61,37 @@ When('je charge les données de test', async function (this: FestipodWorld) {
});
(this as any)._eventCountBefore = countBefore;
await this.appFrame!.evaluate(() => {
// AWAIT the seed's own promise (loadTestData returns a BootstrapResult promise)
// and record whether it actually seeded — so the propagation wait below can tell
// a genuinely-populated wallet (nothing to appear) from an empty one that must
// seed. Fire-and-forget here would let the assertions race the async seed.
const seededResult = await this.appFrame!.evaluate(async () => {
const td = (window as any).__testData;
td.loadTestData();
const r = await td.loadTestData();
return { seeded: r?.seeded ?? false };
});
(this as any)._loadSeeded = seededResult.seeded;
// Wait for data to propagate (if wallet was empty, data should appear)
await this.appFrame!.waitForFunction(
() => {
const td = (window as any).__testData;
// Either data was already there, or it should appear after loading
return td.events.size > 0 || td._loadResult?.seeded === false;
},
{ timeout: 10000 },
).catch(() => {
// Timeout is OK if wallet was already populated (idempotent case)
});
// Wait for data to propagate, and for BOTH events and users — not just events.
// They arrive by two independent paths here: events through the Refine read,
// the seeded profiles through the ORM subscription on the protected scope. If
// only events are awaited, `contient des utilisateurs` asserts before the
// profile read lands and flakes to users:0. On a wallet that already had data
// (seeded === false) there is nothing to wait for. The assertions still verify
// the real counts; this only synchronizes.
if (seededResult.seeded) {
await this.appFrame!.waitForFunction(
() => {
const td = (window as any).__testData;
return td.events.size > 0 && td.users.size > 0;
},
undefined,
{ timeout: 75000 },
).catch(() => {
// Timeout tolerated — the assertions below surface the real failure with a
// clearer message than a raw waitForFunction timeout.
});
}
});
// --- Assertions ---
@@ -13,7 +13,6 @@ import type { FestipodWorld } from '../../../../shared/support/world';
const SCREEN_MARKERS: Record<string, string> = {
'home': 'Festipod',
'events': 'Découvrir',
'login': 'connecter',
'profile': 'Mon profil',
'create-event': "Relayer un événement",
'settings': 'Paramètres',
@@ -35,7 +34,6 @@ function pathForScreen(screenId: string): string {
case 'home': return '/home';
case 'events': return '/events';
case 'create-event': return '/events/new';
case 'login': return '/login';
case 'profile': return '/profile';
case 'edit-profile': return '/profile/edit';
case 'friends-list': return '/profile/friends';
@@ -1,27 +0,0 @@
import { Then } from '@cucumber/cucumber';
import { expect } from 'chai';
import type { FestipodWorld } from '../../../../shared/support/world';
Then('l\'écran gère la redirection automatique après connexion', async function (this: FestipodWorld) {
// Behavioral — covered by the @e2e scenario
// "L'écran de connexion redirige vers l'accueil si déjà connecté".
// At the @ui layer we only verify the screen mounts cleanly.
expect(this.currentScreenId).to.equal('login');
expect(this.renderedDoc, 'Login screen should render').to.not.be.null;
});
Then('l\'écran gère l\'état de connexion en cours', async function (this: FestipodWorld) {
const source = this.getRenderedText();
const hasConnectingState =
source.includes("status === 'connecting'") ||
source.includes("Connexion NextGraph en cours");
expect(hasConnectingState, 'LoginScreen should handle connecting state').to.be.true;
});
Then('l\'écran n\'importe pas de données de démonstration', async function (this: FestipodWorld) {
const source = this.getRenderedText();
const importsSeedData = source.includes('seedData') || source.includes('seedEvents');
const usesFestipodData = source.includes('useFestipodData');
expect(importsSeedData, 'LoginScreen should not import seed data').to.be.false;
expect(usesFestipodData, 'LoginScreen should not use FestipodData context').to.be.false;
});
@@ -74,12 +74,12 @@ export function CreateEventScreen() {
setStep((step - 1) as Step);
};
const submit = () => {
const submit = async () => {
const dateLabel = startDate
? (endDate ? `${startDate} - ${endDate}` : startDate)
: 'Date à définir';
const newEvent = createEvent({
const newEvent = await createEvent({
title: name || 'Nouvel événement',
date: dateLabel,
startDate,
@@ -8,10 +8,13 @@ import type { FestipodWorld } from '../../../../shared/support/world';
// --- Setup ---
Given('un événement {string} existe', async function (this: FestipodWorld, eventTitle: string) {
// Ensure wallet has data (seed if empty)
await this.appFrame!.evaluate(() => {
// Ensure wallet has data (seed if empty). AWAITED, and given a budget that
// matches what seeding costs once every entity is its own document: the seed
// opens five of them, and opening a document takes seconds. The previous 10 s
// was calibrated when seeding meant adding rows to an already-open graph.
await this.appFrame!.evaluate(async () => {
const td = (window as any).__testData;
if (td.events.size === 0) td.loadTestData();
if (td.events.size === 0) await td.loadTestData();
});
// Wait for event to appear
await this.appFrame!.waitForFunction(
@@ -20,7 +23,7 @@ Given('un événement {string} existe', async function (this: FestipodWorld, eve
return [...td.events].some((e: any) => e.title === title);
},
eventTitle,
{ timeout: 10000 },
{ timeout: 75000 },
);
});
+26 -1
View File
@@ -2,13 +2,31 @@ import { useState } from 'react';
import { ArrowLeft } from 'lucide-react';
import { Header, Text, ListItem, Toggle, Divider, BottomNav } from '../../../shared/components/sketchy';
import { useNavigate } from '../../../app/router';
import { useAccount } from '../../../shared/context/AccountContext';
import { logoutNg } from '../../../shared/utils/ngSession';
export function SettingsScreen() {
const navigate = useNavigate();
const { logout } = useAccount();
const [notifications, setNotifications] = useState(true);
const [darkMode, setDarkMode] = useState(false);
const [location, setLocation] = useState(true);
// Faux logout: clears the perceived login (username) only — the shared
// wallet stays open underneath. In staging this returns to ConnexionScreen.
const handleLogout = () => {
logout();
navigate('/');
};
// Real logout (HIDDEN): stops the shared wallet session — forces a broker
// redirect on next access. Stopgap-only escape hatch.
const handleLeaveEnvironment = async () => {
await logoutNg();
logout();
if (typeof window !== 'undefined') window.location.reload();
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Header
@@ -74,9 +92,16 @@ export function SettingsScreen() {
<Divider />
<ListItem onClick={() => navigate('/login')}>
<ListItem onClick={handleLogout}>
<Text style={{ margin: 0, color: '#E53E3E' }}>Se déconnecter</Text>
</ListItem>
{/* Stopgap escape hatch — real wallet logout, kept discreet. */}
<ListItem onClick={handleLeaveEnvironment}>
<Text style={{ margin: 0, fontSize: 12, color: '#bbb' }}>
Quitter l'environnement de test
</Text>
</ListItem>
</div>
<BottomNav active="profile" />
@@ -0,0 +1,68 @@
# language: fr
@data @multibrowser
Fonctionnalité: Harness multi-navigateur — modèles private-wallet et shared-wallet
Pour comparer sereinement les deux modèles de wallet (chacun le sien vs partagé)
En tant que développeur du stopgap puis de la cible NextGraph
Le harness e2e doit piloter plusieurs navigateurs isolés dans un seul scénario,
sous l'un OU l'autre modèle de wallet — deux axes orthogonaux.
# --- Axe machinerie : isolation des contextes (modèle private-wallet) ---
@private-wallet
Scénario: Deux navigateurs avec leur propre wallet ont des stockages locaux indépendants
Étant donné un navigateur "A" avec son propre wallet
Et un navigateur "B" avec son propre wallet
Quand j'écris "valeur-A" sous la clé "sonde" dans le navigateur "A"
Alors la clé "sonde" vaut "valeur-A" dans le navigateur "A"
Et la clé "sonde" est absente dans le navigateur "B"
# Le wallet NextGraph vit sur l'origine du broker (nextgraph.net). Ce scénario
# prouve l'isolation du stockage LÀ, pas seulement sur l'origine locale.
@private-wallet
Scénario: Sur l'origine du broker, deux navigateurs private-wallet restent isolés
Étant donné un navigateur "A" avec son propre wallet
Et un navigateur "B" avec son propre wallet
Quand le navigateur "A" charge l'origine du broker
Et le navigateur "B" charge l'origine du broker
Et j'écris "faux-wallet" sous la clé "ng_probe" dans le navigateur "A"
Alors la clé "ng_probe" vaut "faux-wallet" dans le navigateur "A"
Et la clé "ng_probe" est absente dans le navigateur "B"
# --- Axe wallet : provisioning shared-wallet (injection storageState) ---
# Deux navigateurs distincts portent LE MÊME wallet partagé (injecté au niveau
# harness). Tous deux atteignent l'app connectée à NextGraph sans login manuel.
@shared-wallet
Scénario: Deux navigateurs partageant le wallet se connectent tous deux à NextGraph
Étant donné un navigateur "A" avec le wallet partagé
Et un navigateur "B" avec le wallet partagé
Quand le navigateur "A" charge l'application via le broker
Et le navigateur "B" charge l'application via le broker
Alors le navigateur "A" est connecté à NextGraph
Et le navigateur "B" est connecté à NextGraph
# --- Distribution produit : import ASSISTÉ (pas d'auto-import zéro-touche) ---
#
# L'auto-import zéro-touche par l'app est PROUVÉ IMPOSSIBLE avec le broker
# hébergé : il n'implémente pas l'import inline pendant l'auth web-app et
# renvoie vers nextgraph.eu (cross-origin, non pilotable par Festipod). Voir
# concept nextgraph-platform → knowledge_broker-import-constraint et
# decision_2026-06-17_assisted-wallet-import.
#
# PARCOURS HUMAIN COMPLET — exerce la VRAIE app (staging, gate ON) de bout en
# bout : Festipod propose le FICHIER du portefeuille → l'humain le télécharge et
# l'importe sur nextgraph.eu (« Import a Wallet File » + mot de passe) → revient
# → « Entrer » → connecté. Le FICHIER est la primitive correcte (statique,
# réutilisable) — le TextCode est un transfert temporaire 5 min, inutilisable à
# embarquer (cf. nextgraph-platform → knowledge_broker-import-constraint).
# (≠ scénario @shared-wallet ci-dessus, qui INJECTE le wallet via storageState
# et court-circuite donc l'import — provisioning de TEST, pas le flux produit.)
@shared-wallet @assisted-import @humain
Scénario: Parcours humain — le testeur importe le portefeuille fourni par Festipod et se connecte
Étant donné un nouveau testeur ouvre Festipod en staging sur un navigateur vierge
Alors Festipod affiche l'écran d'accès avec le portefeuille à télécharger
Quand le testeur télécharge le portefeuille et l'importe sur nextgraph.eu
Et le testeur revient sur Festipod et clique « Entrer »
Alors Festipod est connecté et propose de choisir un nom d'utilisateur
Quand le testeur choisit un nom d'utilisateur
Alors il arrive sur l'accueil de l'application
@@ -0,0 +1,28 @@
# language: fr
@WORKSHOP @priority-1
Fonctionnalité: Stopgap multi-store — primitives de données
En tant que développeur
Je veux valider, contre le vrai broker NextGraph, les primitives du stopgap
wallet partagé (création de documents, ORM sur un document créé, aller-retour
du sharedWalletShim) avant d'activer le mode multi-document.
# --- Data (broker réel) ---
@data
Scénario: L'ORM lit et écrit dans un document créé par doc_create
Étant donné un nouveau document de graphe est créé dans le wallet partagé
Quand j'écris une participation dans ce document via l'ORM
Alors la participation est lisible dans ce document
@data
Scénario: Le sharedWalletShim fait l'aller-retour par le wallet
Étant donné un compte "@smoketest" est enregistré dans le shim
Alors le compte "@smoketest" est retrouvé après rechargement du shim
Et le compte "@smoketest" possède trois documents de périmètre distincts
@data
Scénario: Lecture fan-out sur plusieurs documents d'entité (1 doc par entité)
Étant donné deux comptes ayant chacun un document d'événement indexé
Quand j'écris un événement dans chacun de ces deux documents
Alors un abonnement multi-graphes lit les deux événements ensemble
Et l'index public liste les deux documents
@@ -0,0 +1,147 @@
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from 'chai';
import type { FestipodWorld } from '../../../../shared/support/world';
import { pool } from '../../../../shared/support/browserPool';
// Multi-browser harness steps. Two ORTHOGONAL axes:
// - browser count : several named, isolated browsers in one scenario;
// - wallet model : 'own' (its own/no wallet) vs 'shared' (THE shared wallet).
// The wallet model is carried explicitly by the Given phrasing.
// See brief_2026-06-15_shared-wallet-shim.
Given('un navigateur {string} avec son propre wallet', async function (this: FestipodWorld, name: string) {
const handle = await this.openBrowser(name, 'own');
// Land on the local harness origin (no NG stack) so localStorage is available.
await handle.page.goto(`${pool.harnessUrl}/blank`, { waitUntil: 'domcontentloaded' });
});
Given('un navigateur {string} avec le wallet partagé', async function (this: FestipodWorld, name: string) {
const handle = await this.openBrowser(name, 'shared');
await handle.page.goto(`${pool.harnessUrl}/blank`, { waitUntil: 'domcontentloaded' });
});
// --- Parcours HUMAIN complet (e2e fidèle) ---
// Ouvre la VRAIE app en staging (gate ON), lit le code À L'ÉCRAN, l'importe sur
// nextgraph.eu, revient, clique « Entrer » → app connectée. C'est la garantie
// que Festipod remet à l'humain un code qui marche. Browser fixe "H".
Given('un nouveau testeur ouvre Festipod en staging sur un navigateur vierge', async function (this: FestipodWorld) {
const url = await pool.ensureStagingApp();
(this as any).stagingUrl = url;
const handle = await this.openBrowser('H', 'own'); // vierge, AUCUN wallet
await handle.page.goto(url, { waitUntil: 'domcontentloaded' });
// L'écran d'accès (AccessGateScreen) doit s'afficher.
await handle.page.getByText('Entrer', { exact: true }).waitFor({ state: 'visible', timeout: 15000 });
});
Then('Festipod affiche l\'écran d\'accès avec le portefeuille à télécharger', async function (this: FestipodWorld) {
const page = this.browser('H').page;
// Le fichier du portefeuille est proposé au téléchargement…
const downloadVisible = await page.locator('[data-testid=shared-wallet-download]').isVisible();
expect(downloadVisible, 'le bouton de téléchargement du portefeuille doit être affiché').to.equal(true);
// …et le mot de passe partagé est affiché (c'est bien CELUI du wallet e2e).
const pwd = (await page.locator('[data-testid=shared-wallet-password]').innerText()).trim();
expect(pwd, 'le mot de passe affiché doit être celui du wallet partagé').to.equal(pool.sharedWalletPassword);
(this as any).displayedPassword = pwd;
});
When('le testeur télécharge le portefeuille et l\'importe sur nextgraph.eu', async function (this: FestipodWorld) {
const page = this.browser('H').page;
// Télécharge le fichier DEPUIS l'écran Festipod (le vrai geste humain)…
const [download] = await Promise.all([
page.waitForEvent('download'),
page.locator('[data-testid=shared-wallet-download]').click(),
]);
const filePath = await download.path();
// …et l'importe via "Import a Wallet File" avec le mot de passe lu à l'écran.
await pool.importWalletViaFile(page, filePath!, (this as any).displayedPassword);
});
When('le testeur revient sur Festipod et clique « Entrer »', async function (this: FestipodWorld) {
const handle = this.browser('H');
await handle.page.goto((this as any).stagingUrl, { waitUntil: 'domcontentloaded' });
const entrer = handle.page.getByText('Entrer', { exact: true });
await entrer.waitFor({ state: 'visible', timeout: 15000 });
await entrer.click(); // déclenche le redirect vers le broker
await handle.page.waitForURL('**nextgraph**', { timeout: 20000 }).catch(() => {});
// Le broker demande de déverrouiller le wallet fraîchement importé → son mot de
// passe (completeBrokerLogin attend la page de login wallet de façon robuste).
handle.appFrame = await pool.completeBrokerLogin(handle.page, (this as any).stagingUrl, pool.sharedWalletPassword);
});
Then('Festipod est connecté et propose de choisir un nom d\'utilisateur', async function (this: FestipodWorld) {
const handle = this.browser('H');
expect(handle.appFrame, 'l\'app doit être chargée dans l\'iframe broker').to.not.equal(null);
// Connecté → AuthGate passe l'AccessGateScreen et montre ConnexionScreen.
await handle.appFrame!.waitForFunction(
() => /Choisissez votre nom d'utilisateur|Connexion/.test(document.body?.innerText ?? ''),
{ timeout: 30000 },
);
});
When('le testeur choisit un nom d\'utilisateur', async function (this: FestipodWorld) {
const frame = this.browser('H').appFrame!;
await frame.locator('input[placeholder="@votrepseudo"]').fill('@testeur');
await frame.getByRole('button', { name: 'Se connecter' }).click();
});
Then('il arrive sur l\'accueil de l\'application', async function (this: FestipodWorld) {
const frame = this.browser('H').appFrame!;
// On atterrit sur /home (l'app), PAS sur l'onboarding hors-connexion ('/'
// WelcomeScreen « Rejoindre la communauté »).
await frame.waitForFunction(
() => window.location.pathname.endsWith('/home') &&
!(document.body?.innerText ?? '').includes('Rejoindre la communauté'),
{ timeout: 15000 },
);
});
When('le navigateur {string} charge l\'application via le broker', async function (this: FestipodWorld, name: string) {
// Drive the named browser through the broker into the NG harness iframe.
// A 'shared' browser carries the wallet (storageState) → the broker recognises
// it; an 'own' browser has none → the broker cannot reach the app.
await this.loadAppInBrowser(name, 'harness');
});
Then('le navigateur {string} est connecté à NextGraph', async function (this: FestipodWorld, name: string) {
const handle = this.browser(name);
expect(handle.appFrame, `le navigateur ${name} doit avoir chargé l'app`).to.not.equal(null);
// __testData.ready flips true only once the NG session is connected.
await handle.appFrame!.waitForFunction(
() => (window as any).__testData?.ready === true,
{ timeout: 30000 },
);
});
When('le navigateur {string} charge l\'origine du broker', async function (this: FestipodWorld, name: string) {
// Navigate top-level to the broker origin (nextgraph.net) — the exact origin
// where the NG wallet localStorage lives. Each fresh context has its own
// storage partition there too.
await this.browser(name).page.goto(pool.brokerOrigin, { waitUntil: 'domcontentloaded' });
});
When(
'j\'écris {string} sous la clé {string} dans le navigateur {string}',
async function (this: FestipodWorld, value: string, key: string, name: string) {
await this.browser(name).page.evaluate(
([k, v]) => localStorage.setItem(k, v),
[key, value] as [string, string],
);
},
);
Then(
'la clé {string} vaut {string} dans le navigateur {string}',
async function (this: FestipodWorld, key: string, expected: string, name: string) {
const actual = await this.browser(name).page.evaluate((k) => localStorage.getItem(k), key);
expect(actual, `localStorage["${key}"] dans le navigateur ${name}`).to.equal(expected);
},
);
Then(
'la clé {string} est absente dans le navigateur {string}',
async function (this: FestipodWorld, key: string, name: string) {
const actual = await this.browser(name).page.evaluate((k) => localStorage.getItem(k), key);
expect(actual, `localStorage["${key}"] dans le navigateur ${name} doit être isolé`).to.equal(null);
},
);

Some files were not shown because too many files have changed in this diff Show More