Run locally like the sibling apps: fixed ports, the pod provider in Docker, the app outside

Festipod now follows the local convention of welcometomyplace, mutual-aid.app
and portejunes: one fixed port pair per app (Festipod defaults: backend 3005,
app 4005; pod provider 3000/5000), overridable in backend/.env and .env.
`make start` runs docker-compose-dev.yml (the local pod provider from images,
Fuseki, Redis, mailcatcher, Arena); the backend and the app run outside Docker
with `cd backend && pnpm dev` and `pnpm dev`. pnpm stays the package manager.

The old docker-compose.yml, which containerized Festipod's own frontend and
backend and could not reach the host's Fuseki and Redis, is gone; portless is
dropped from the dev script, since the backend declares a single OIDC redirect
URI. The local backend runs without a Redis cache, as the sibling apps do. The
e2e suites and validate read the app, backend and pod provider URLs from the
env files instead of hardcoding them.

The deployment and local cookbook leaves record the traps met while resetting
datasets: a dataset re-created under the same name needs a Fuseki restart
(ClosedException otherwise), the anonymous WebACL probe at boot is expected,
and a change of the backend's URL leaves persistent accounts with a
participations collection the new app identity cannot control.
This commit is contained in:
Sylvain Duchesne
2026-11-01 11:40:27 +01:00
parent 908f7ea973
commit 5e3e821e2a
33 changed files with 280 additions and 129 deletions
+5 -36
View File
@@ -1,7 +1,7 @@
# Festipod — variables d'environnement (exemple)
#
# Copier en `.env` et renseigner les valeurs.
# En dev (`bun run dev`) ET en prod (`bun run start`), l'app sert depuis src/ et
# En dev (`pnpm dev`) ET en prod (`bun run start`), l'app sert depuis src/ et
# lit ces variables au RUNTIME (via l'endpoint /festipod-config.json de src/index.ts).
#
# REQUIS POUR LES TESTS. La suite Cucumber tourne sous `node` (pas sous Bun), qui
@@ -37,48 +37,17 @@ FESTIPOD_SHARED_WALLET_FILE_BASE64=
FESTIPOD_AUTO_SEED=
# ── Serveur ────────────────────────────────────────────────────────────────
# Port HTTP du serveur (4000 : l'app est servie sur l'origine :4000 de la pile locale).
PORT=4000
# Port HTTP du serveur (4005 : l'app Festipod, paire 3005/4005 des apps sœurs).
PORT=4005
# NODE_ENV=production bascule `bun run start` (pas de HMR). En dev, laisser vide.
NODE_ENV=
# ── Backend ActivityPods ───────────────────────────────────────────────────
# URL publique du backend Festipod (écoute sur le port 3003).
SEMAPPS_HOME_URL=http://localhost:3003
# URL publique du frontend (utilisée par le backend pour les OIDC redirects, etc.)
SEMAPPS_FRONT_URL=http://localhost:4000
# Endpoint SPARQL du Fuseki auquel se connecte le backend.
SEMAPPS_SPARQL_ENDPOINT=http://localhost:3030/
# Dataset Jena/Fuseki principal utilisé par le backend.
SEMAPPS_MAIN_DATASET=festipod
# Identifiants Jena/Fuseki.
SEMAPPS_JENA_USER=admin
SEMAPPS_JENA_PASSWORD=admin
# Redis : cache (db 3) et file des jobs (db 4).
SEMAPPS_REDIS_CACHE_URL=redis://localhost:6390/3
SEMAPPS_QUEUE_SERVICE_URL=redis://localhost:6390/4
# Dataset utilisé par le service auth.account.
SEMAPPS_AUTH_ACCOUNTS_DATASET_NAME=settings-festipod
# Dépôt de shapes/shapetrees utilisé pour déclarer les besoins d'accès.
SEMAPPS_SHAPE_REPOSITORY_URL=https://shapes.activitypods.org/
# Nom et description de l'application (backend).
SEMAPPS_APP_NAME=Festipod
SEMAPPS_APP_DESCRIPTION=Points de rencontre sur les événements publics
SEMAPPS_APP_LANG=en,fr
SEMAPPS_PORT=3003
# Le backend lit son propre backend/.env (voir backend/.env.example).
# ── Frontend / Backend coupling ────────────────────────────────────────────
# URL du backend telle que vue par le navigateur.
VITE_BACKEND_URL=http://localhost:3003
VITE_BACKEND_URL=http://localhost:3005
# Client ID OAuth/Solid du backend (laisser vide pour dériver `${VITE_BACKEND_URL}/as/actor/app`).
# Le client_id DOIT être l'URI de l'acteur app : le pod enregistre l'app sous cette URI et
+3
View File
@@ -45,5 +45,8 @@ dist-staging/
*.ngw
.tasks/
# Local data of the dev stack (docker-compose-dev.yml): fuseki, redis, pod provider
/data
# Per-developer contract access map (canonical provider → local checkout) — never committed.
.project/contracts.local.yaml
@@ -5,19 +5,34 @@ summary: Bring up a local ActivityPods pod provider for Festipod development —
# Cookbook — Local ActivityPods platform
Platform checkout: `/home/sylvain/projects/activitypods/activitypods` (never modify it; verified pristine 2026-10-09). Everything runs from `pod-provider/`.
Festipod's local setup follows the sibling ActivityPods apps (welcometomyplace, mutual-aid.app, portejunes): a local pod provider and its infrastructure run from images through `make start`, and the app's own backend and frontend run outside Docker, one command each.
## Ports
pod provider 3000 (OpenID + `/auth` signup/login), pod provider frontend 5000, Festipod backend 3003, Festipod app 4000, fuseki 3030 (admin/admin), mailcatcher 1080, redis 6390, arena 4567, tripleadmin 3033. The pod provider frontend on :5000 must run whenever Festipod signs a user in through the browser: the pod's login and app-consent screens are its pages (the UI smoke spawns its own app and does not need it; `test:e2e-flow`, `test:e2e-rights` and `test:e2e-revocation` do). The Festipod app stays on :4000, the only registered OIDC redirect origin.
One fixed port pair per app on localhost, the pod provider keeping 3000/5000:
| Service | Port |
|---|---|
| pod provider (OpenID + `/auth` signup/login) / its frontend | 3000 / 5000 |
| welcometomyplace backend / frontend | 3002 / 4002 |
| mutual-aid backend / frontend | 3003 / 4003 |
| portejunes backend / frontend | 3004 / 4004 |
| **Festipod backend / app** (committed defaults) | **3005 / 4005** |
| fuseki (admin/admin) | 3030 |
| redis | 6379 |
| mailcatcher | 1080 (SMTP 1025) |
| arena | 4567 |
A developer whose ports differ overrides them in the two gitignored env files, never in committed code: `backend/.env` (`SEMAPPS_PORT`, `SEMAPPS_HOME_URL`, `SEMAPPS_FRONT_URL`, the Redis and Fuseki endpoints) and the root `.env` (`PORT` for the app, `VITE_BACKEND_URL`, `VITE_POD_PROVIDER_URL`). The e2e suites and `scripts/validate.ts` read the same values from the environment (`PORT` → app URL, `VITE_BACKEND_URL`, `VITE_POD_PROVIDER_URL`), falling back to 3005 / 4005 / 3000. The app origin (`FRONT_URL`) is the only registered OIDC redirect origin, so the app must be served exactly there. The pod provider frontend on :5000 must run whenever Festipod signs a user in through the browser: the pod's login and app-consent screens are its pages (the UI smoke spawns its own app and does not need it; `test:e2e-flow`, `test:e2e-rights` and `test:e2e-revocation` do).
## Bring up
1. Infra: `cd pod-provider && docker compose up -d`. If host port 6379 is already taken, add an override file publishing redis on another port (6390 was used) and pass `SEMAPPS_QUEUE_SERVICE_URL=redis://localhost:6390/1` and `SEMAPPS_REDIS_OIDC_PROVIDER_URL=redis://localhost:6390/2` when starting the backend — dotenv-flow does not override the process environment.
2. Install once: `cd backend && COREPACK_ENABLE_AUTO_PIN=0 corepack yarn install --frozen-lockfile`, same in `frontend/`. Keep `COREPACK_ENABLE_AUTO_PIN=0`: corepack auto-pin rewrites `packageManager` in package.json. Note: the pod-provider README says `yarn start` for the frontend; only the `dev` script exists — use `BROWSER=none corepack yarn dev`.
3. Backend: `cd backend && corepack yarn start`. Wait for "ServiceBroker with ... service(s) started successfully" (~11 s).
4. Frontend: `cd frontend && VITE_CONFIG_URL=http://localhost:3000/.well-known/config.js BROWSER=none corepack yarn dev`. Without `VITE_CONFIG_URL` the page's `window.CONFIG` stays undefined and the consent flow dies with an opaque "Failed to fetch". Verify: `GET http://localhost:5000/` → 200 and `GET http://localhost:3000/.well-known/config.js` → 200.
5. Festipod stack (this repo root): `cp backend/.env.example backend/.env` before `docker compose up`. The compose reads `backend/.env` as an optional `env_file`; on a fresh clone the file is gitignored and absent, so this copy is what feeds the backend its ActivityPods settings. With the coherent layout the app is served on :4000 and the Festipod backend on :3003 (the pod provider frontend keeps :5000 and must run for any browser sign-in, see Ports). To serve the app manually (`bun run start` on :4000) ALSO copy the root env: `cp .env.example .env` — without `VITE_BACKEND_URL` the config route serves an empty backend URL and the app deliberately falls back to the local demo (seeded data, no login).
1. Once: `cp backend/.env.example backend/.env` and `cp .env.example .env` (both gitignored; fill the shared-wallet values in the root one), then `pnpm install` at the root and in `backend/`.
2. Pod provider and infrastructure: `make start` (`docker-compose-dev.yml`: `activitypods/backend` in host network mode on 3000, `activitypods/frontend` on 5000, `semapps/jena-fuseki-webacl` on 3030 with `./data/fuseki`, `redis` on 6379 with `./data/redis`, mailcatcher, `activitypods/arena` configured by `docker/arena-dev.json`). Other targets: `make stop`, `make config`, `make upgrade`, `make attach-activitypods`. `./data/` is gitignored.
3. Festipod backend: `cd backend && pnpm dev`. Wait for "ServiceBroker with ... service(s) started successfully" (~11 s).
4. Festipod app: `pnpm dev` at the root (the frontend lives at the root, not in a `frontend/` directory; the root scripts run Bun, pnpm only launches them).
`backend/.env` follows the sibling layout: own dataset `festipod` and accounts dataset `settings-festipod`, `SEMAPPS_REDIS_CACHE_URL=` empty (no cache locally), queue on `redis://localhost:6379/15` (the siblings use dbs 12, 13 and 14). dotenv-flow does not override the process environment, so a variable exported in the shell wins over the file. If host port 6379 is already taken, publish redis elsewhere and point `SEMAPPS_QUEUE_SERVICE_URL` at it in `backend/.env`.
## Account and resource flow (all VERIFIED 2026-10-09)
@@ -33,18 +48,18 @@ On a fresh pod, an `as:Event` POST lands at the pod root (`/alice/data/<uuid>`):
## Data location (hard-won, 2026-10-26)
- **Fuseki's data dir must stay on the SSD** (`/home`, NVMe). Measured: a signup is ~10 s on the SSD, 150–200 s on the spinning HDD (`/hdd-home`) — TDB2 fsyncs per write and the seek time dominates. The HDD mount also hosts the bulk of the host's files; keep fuseki off it.
- **Fuseki's data dir (`./data/fuseki`) must stay on the SSD** (`/home`, NVMe). Measured: a signup is ~10 s on the SSD, 150–200 s on the spinning HDD (`/hdd-home`) — TDB2 fsyncs per write and the seek time dominates. The HDD mount also hosts the bulk of the host's files; keep fuseki off it.
- **Never replace `data/fuseki` with a symlink after the container exists.** Docker resolves the bind source when the container is CREATED; moving the data away and leaving a link behind leaves the running container with a broken/empty mount (symptom: signup 500 « Error when creating secure dataset … FileNotFoundException: /fuseki/system_files/… »). `docker restart fuseki` re-resolves and heals — until the next behind-its-back move.
- The e2e suites delete the accounts they create at the end of every run (user decision 2026-10-26): `DELETE http://localhost:3000/.account/:username` with the user's token (a user may delete only their own account). The platform tombstones the account, sends the Delete activity, and drops the fuseki dataset on a delay — so test data no longer accumulates and the periodic full wipe is no longer needed. `diag-invite.ts` is the only suite left without cleanup (manual diagnostic).
## Backend fresh-boot note
The Festipod backend has its OWN datasets, `festipod` (main, created SECURE at first boot — WebACL is on) and `settings-festipod` (accounts), named like the sibling apps' (`welcometomyplace`/`settings-wtmp`…). Never point it at the pod provider's `settings` dataset, and never delete or recreate `settings`: it holds the pod provider's accounts. A backend dataset reset follows the compose file's header (delete the datasets and the secure dataset's files on disk, empty the backend's Redis cache database). After a reset, pod accounts that had installed the app must install it again: the new backend knows none of their registrations.
The Festipod backend has its OWN datasets, `festipod` (main, created SECURE at first boot — WebACL is on) and `settings-festipod` (accounts), named like the sibling apps' (`welcometomyplace`/`settings-wtmp`…). Never point it at the pod provider's `settings` dataset, and never delete or recreate `settings`: it holds the pod provider's accounts. A backend dataset reset: delete the two datasets and the secure dataset's files on disk (`./data/fuseki`), and empty the backend's Redis queue database. After a reset, pod accounts that had installed the app must install it again: the new backend knows none of their registrations. A deleted dataset re-created under the same name needs a Fuseki restart in between (`docker restart fuseki`), else every query fails with `ClosedException: already closed`. Changing `SEMAPPS_HOME_URL` (a new port, say) changes the app actor's URI, hence the app's identity: a persistent account keeps a participations collection whose `acl:Control` belongs to the previous app actor, so the backend refuses every Join of that account ("the pod refused the read grant of the contacts group…") — remove the `http://festipod.org/participations` link from that account's profile with its own token (`DELETE DATA` of the exact triple; `DELETE WHERE` answers 400) and the next Join creates a new collection.
On a FRESH backend dataset (after a wipe), the app's actor and its access-need group are created in parallel by the mixin's boot and can race: the actor can end up without `interop:hasAccessNeedGroup` (symptom: `GET /app` links a group that 404s; consents register zero grants). `backend/services/app.service.js` heals this at boot by polling for the group (up to 60 s) and attaching it. If the backend is restarted while its dataset is wiped, wait for the heal before running consented flows — or simply re-check `GET /app` links a group that resolves 200.
## Teardown
`pkill -f moleculer-runner; pkill -f react-scripts`, then `docker compose down` in `pod-provider/`. Data persists in `pod-provider/data/` (gitignored); delete that directory to fully reset. With the suites cleaning up after themselves (above), a full reset should now be a rare event, not a maintenance ritual.
Stop the two dev processes (`pkill -f moleculer-runner`, and the app's `bun --hot`), then `make stop`. Data persists in `./data/` (gitignored); delete that directory to fully reset. With the suites cleaning up after themselves (above), a full reset should now be a rare event, not a maintenance ritual.
Current stack test account: `alice` / `Test1test` on localhost:3000.
+1 -1
View File
@@ -18,4 +18,4 @@ The project's stack and tooling. Guiding principle: **Bun-first** — Bun replac
- [[knowledge_bun-apis]] — native Bun APIs (serve, sqlite, redis, sql, file, shell)
- [[knowledge_build-pipeline]] — build.ts, bundler, server, harness built separately, Storybook
- [[knowledge_stack-and-commands]] — stack components + the real scripts (plus quirks)
- [[knowledge_deployment]] — the Coolify test stack (one base URL, backend under /api), both images, first-boot traps, prod served from src/, `portless` in dev
- [[knowledge_deployment]] — the Coolify test stack (one base URL, backend under /api), both images, first-boot traps, prod served from src/
@@ -1,6 +1,6 @@
---
type: caveat
summary: A running `bun run dev` never picks up a refreshed data-layer package — VERIFIED, not even across a real rebuild (new bundle hash, still the stale dependency). Only a restart serves the fresh copy; suspect a stale server before suspecting the code.
summary: A running `pnpm dev` never picks up a refreshed data-layer package — VERIFIED, not even across a real rebuild (new bundle hash, still the stale dependency). Only a restart serves the fresh copy; suspect a stale server before suspecting the code.
last_checked: 2026-08-16
---
@@ -10,7 +10,7 @@ last_checked: 2026-08-16
**VERIFIED, controlled sandbox test with this project's own bun.** A dependency resolving to copy A, overlaid with copy B: the running server still serves A at +3 s and +13 s after the overlay. An edit to **application source** then triggers a genuine rebuild — a new bundle hash confirms it — and the rebuilt bundle **still serves A**. Only a restart serves B. So the mechanism is not "the watcher never fires because `node_modules` is excluded" — a rebuild the watcher DOES trigger still carries the stale dependency forward; the server's resolution of that import is pinned at process start, and a rebuild does not re-resolve it.
**So: restart `bun run dev` after every refresh of the package — a rebuild is not a substitute, even a real one.** There is no signal that you needed to; a stale server looks exactly like a current one.
**So: restart `pnpm dev` after every refresh of the package — a rebuild is not a substitute, even a real one.** There is no signal that you needed to; a stale server looks exactly like a current one.
## Why this is worth a leaf
@@ -10,7 +10,7 @@ last_checked: 2026-08-16
| Path | Command | What is served |
|---|---|---|
| **Dev** | `bun run dev` → `bun --hot src/index.ts` | **`src/`** — HMR, port 3000 |
| **Dev** | `pnpm dev` → `bun --hot src/index.ts` | **`src/`** — HMR, port `PORT` (default 4005) |
| **Production** | `bun run start` → `NODE_ENV=production bun src/index.ts` | **`src/` as well** — Bun transpiles on the fly |
| Bundle | `bun run build` → `build.ts` (Bun bundler + Tailwind plugin) → `dist/` | **nothing** |
@@ -40,7 +40,7 @@ Practical consequence: to exercise the "shared wallet" flow in dev **end to end*
```
FESTIPOD_SHARED_WALLET_PASSWORD=festipod-e2e-tests \
FESTIPOD_SHARED_WALLET_FILE=./festipod-e2e-tests.ngw \
bun run dev
pnpm dev
```
## The test harness is built separately
@@ -12,11 +12,13 @@ last_checked: 2026-10-19
**One base URL per environment** (user decision 2026-10-19, so a pull-request preview lives on a single host, like the sibling apps): the app at `https://festipod.dev.reconnexion.coop/`, the backend on the same host under `/api` — its Domains field is `https://<host>/api,https://<host>/.well-known`. Coolify's **Strip Prefixes must be OFF** (it is on the sibling apps): the backend mounts its routes under `/api` itself, read from `SEMAPPS_HOME_URL=https://<host>/api/`, and the front end is pointed at `VITE_BACKEND_URL=https://<host>/api`. With the prefix stripped every backend request is refused (404).
**The backend is base-path aware** (`backend/services/base-path.js`), because SemApps is: it mounts every route it adds under the base URL's path (LDP resources, the app actor and its inbox, public keys, access needs, webhooks, `/nodeinfo/2.1`) but keeps the `/.well-known` ones (`context.jsonld`, `nodeinfo`, `solid`, `void`) at the origin root. Festipod's own routes (`/app`, `/as/actor/app`, `/catalog`) are mounted under the base path the same way, the public-surface allow-list matches paths relative to it (the two `/.well-known` endpoints against the full path), and the Solid-OIDC `client_id` is the requested URI rebuilt from the base URL as written — so it equals, byte for byte, the actor URI the backend mints and the `${backendUrl}/as/actor/app` the front end derives. At the root (`http://localhost:3003`, local dev) every decision is identical to the pre-base-path code. To verify: `bun test backend/services` (with and without `SEMAPPS_HOME_URL=https://<host>/api/`).
**The backend is base-path aware** (`backend/services/base-path.js`), because SemApps is: it mounts every route it adds under the base URL's path (LDP resources, the app actor and its inbox, public keys, access needs, webhooks, `/nodeinfo/2.1`) but keeps the `/.well-known` ones (`context.jsonld`, `nodeinfo`, `solid`, `void`) at the origin root. Festipod's own routes (`/app`, `/as/actor/app`, `/catalog`) are mounted under the base path the same way, the public-surface allow-list matches paths relative to it (the two `/.well-known` endpoints against the full path), and the Solid-OIDC `client_id` is the requested URI rebuilt from the base URL as written — so it equals, byte for byte, the actor URI the backend mints and the `${backendUrl}/as/actor/app` the front end derives. At the root (`http://localhost:3005`, local dev) every decision is identical to the pre-base-path code. To verify: `bun test backend/services` (with and without `SEMAPPS_HOME_URL=https://<host>/api/`).
**First-boot traps.**
- The backend registers its app actor in its dataset with the domain it has on the FIRST boot: set the domains in Coolify before the first deployment.
- The backend runs with WebACL on, as the reference apps do, so its main dataset must be SECURE: the backend creates it so when it does not exist, and refuses to start ("the main dataset is not secure") on an existing one created otherwise. Starting again from nothing (domains changed, or a dataset created before WebACL was on): delete the two datasets in Fuseki AND the secure dataset's files Fuseki's delete leaves on disk (`databases/<main>`, `<main>Acl`, `<main>Mirror`, `configuration/<main>.ttl`), empty the stack's Redis (its cache keeps serving the old app actor, keys and access needs), redeploy — then every user installs the app again. The exact steps are in the compose file's header.
- The backend runs with WebACL on, as the reference apps do, so its main dataset must be SECURE: the backend creates it so when it does not exist, and refuses to start ("the main dataset is not secure") on an existing one created otherwise. Starting again from nothing (domains changed, or a dataset created before WebACL was on): delete the two datasets in Fuseki AND the secure dataset's files Fuseki's delete leaves on disk (`databases/<main>`, `<main>Acl`, `<main>Mirror`, `configuration/<main>.ttl`), empty the stack's Redis (its cache keeps serving the old app actor, keys and access needs), redeploy — then every user installs the app again. The exact steps are in the compose file's header. Two more traps, both met on the test server 2026-10-30:
- **A deleted dataset cannot be re-created under the same name in the same Fuseki process**: Jena keeps the closed storage of that location, and every query on the new dataset fails with `ClosedException: already closed` (500), so the backend crashes at boot. Restart Fuseki before re-creating it — on the test server it is the shared one, so every test app loses its triplestore meanwhile — or give the stack new dataset names (`MAIN_DATASET` / `SETTINGS_DATASET`, e.g. `festipod-test-2`).
- A "Model permissions violation: http://semapps.org/securedWebacl" in Fuseki's log at the backend's boot is EXPECTED: semapps probes the WebACL graph anonymously to check the dataset is secure and wants that refusal. The backend must reach Fuseki as the Fuseki user named exactly `admin` (`JENA_USER`), the only one the semapps permissions module grants a secure dataset to (semapps `ShiroEvaluator`, read in source).
- On a cold first boot, `GET <base>app` answers from an in-memory copy of the actor taken before the access-need group got linked, so it lacks `interop:hasAccessNeedGroup` until the backend restarts (the actor itself, `<base>as/actor/app`, is right). A real sign-in is unaffected — the pod reads the actor — but the rights matrix's programmatic install reads `/app`: restart the backend once after a first deployment.
**Verified 2026-10-19, locally, not yet on Coolify**: both images built from a clean export of the tree, run against the local pod provider behind a same-origin proxy that routes `/api` and `/.well-known` to the backend unstripped with the Host header kept (as Traefik does) — the login flow, the rights matrix (98/98 hard lines) and the revocation probe all pass; the same images at the origin root pass them too.
@@ -56,7 +58,7 @@ A `Dockerfile` exists (multi-stage Bun Alpine). **Installation goes through pnpm
## Environment variables
- `PORT` (default 3000), `NODE_ENV` (enables/disables HMR and the dev auto-seed — see concept `data-layer`).
- `PORT` (default 4005), `NODE_ENV` (enables/disables HMR and the dev auto-seed — see concept `data-layer`).
- No `.env*` is committed (`.env` is gitignored).
### The shared wallet: config, not a secret, not a mount
@@ -73,6 +75,6 @@ A `Dockerfile` exists (multi-stage Bun Alpine). **Installation goes through pnpm
## Dev
`bun run dev` = **`portless festipod bun --hot src/index.ts`** — it goes through the **`portless`** wrapper (an external port-management tool), not a bare `bun --hot`. HMR is active outside production.
`pnpm dev` = **`bun --hot src/index.ts`**, served on the fixed port `PORT` (root `.env`, default 4005) like the sibling ActivityPods apps — no port-management wrapper. HMR is active outside production. The local stack (pod provider, backend, ports) is in [[cookbook_local-activitypods-platform]].
**Reactive local overlay for the SDK**: in production the `@ng-eventually/polyfill` dependency comes from Gitea (git+https, pinned by `pnpm-lock.yaml`), and `@ng-helpers/indexing` likewise. When a provider's package has to be exercised from a local checkout, `pnpm run overlay:polyfill` or `pnpm run overlay:indexing` (script `scripts/overlay-local-checkout.ts`, one provider per run) replaces `node_modules/<package>` with a **real copy** of that checkout (location overridable with `NG_EVENTUALLY_LOCAL` / `NG_HELPERS_LOCAL`) — **without** its own `node_modules/*` — and resyncs on every edit. Copying rather than symlinking is what keeps a **single instance** of every package the provider shares with Festipod installed (`@ng-org/*`, and for `indexing`, `@ng-eventually/polyfill` itself): a symlink would drag in a second one and the SDK would stop working. To go back to the committed state: `pnpm install`.
@@ -21,8 +21,8 @@ summary: Stack components (Bun runtime/build/test, install through pnpm, React,
| Script | Command / role |
|---|---|
| `dev` | `portless festipod bun --hot src/index.ts` — dev with HMR through the `portless` wrapper (see [[knowledge_deployment]]) |
| `start` | `NODE_ENV=production bun src/index.ts` — production, served from `src/` (not `dist/`) |
| `dev` | `bun --hot src/index.ts` — dev with HMR on the fixed port `PORT` (root `.env`, default 4005); launched with `pnpm dev`, run by Bun |
| `start` | `NODE_ENV=production bun src/index.ts` — production, served from `src/` (not `dist/`), same `PORT` default (4005) |
| `build` | `bun run build.ts` — Bun bundler + Tailwind → `dist/`, **which nothing serves**: production runs `start`, from `src/` ([[knowledge_build-pipeline]]) |
| `test:cucumber` | chains `cucumber:run` → `cucumber:report` → `features:parse` → `steps:extract` |
| `cucumber:run` | `node --import tsx/esm node_modules/@cucumber/cucumber/bin/cucumber.js` — **through Node+tsx, not Bun** (Playwright/happy-dom plugin compatibility), and through the package's **actual JS entry**, not the `.bin/` shim (see Pitfalls) |
+1 -1
View File
@@ -12,7 +12,7 @@ Web app mobile-first où les utilisateurs créent des **points de rencontre** qu
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` (portless, port dynamique ; défaut servi par `bun run start` : 4000).
- **Bun-first** : `bun` / `bun install` / `bun test` / `bun build`, jamais node/npm/vite/jest. `pnpm dev` (port fixe, `PORT` du `.env` racine, défaut 4005 ; même défaut pour `pnpm start`).
## Frontière SDK
+22
View File
@@ -0,0 +1,22 @@
.PHONY: start stop config upgrade attach-activitypods
# The local pod provider and its infrastructure (fuseki, redis, mailcatcher, arena).
# Festipod's own backend and app run outside Docker: `cd backend && pnpm dev`, `pnpm dev`.
DOCKER_COMPOSE_DEV=docker compose -f docker-compose-dev.yml
start:
$(DOCKER_COMPOSE_DEV) up -d
stop:
$(DOCKER_COMPOSE_DEV) kill
$(DOCKER_COMPOSE_DEV) rm -fv
config:
$(DOCKER_COMPOSE_DEV) config
upgrade:
$(DOCKER_COMPOSE_DEV) pull
$(DOCKER_COMPOSE_DEV) up -d
attach-activitypods:
$(DOCKER_COMPOSE_DEV) exec activitypods-backend pm2 attach 0
+9 -2
View File
@@ -60,11 +60,18 @@ Identifiées comme nécessaires (notamment pour la scalabilité et la découvert
## Quick Start
Comme les autres apps ActivityPods : le Pod provider local et l'infrastructure tournent dans Docker, le backend et l'app hors Docker. Ports par défaut : Pod provider 3000 (frontend 5000), backend Festipod 3005, app 4005 — à surcharger dans `backend/.env` et `.env`.
```bash
bun install
bun run dev # Dev server avec HMR (port 3000)
cp .env.example .env && cp backend/.env.example backend/.env
make start # Pod provider local + Fuseki + Redis + mailcatcher + Arena
pnpm install && (cd backend && pnpm install)
cd backend && pnpm dev # backend (port SEMAPPS_PORT, 3005 par défaut)
pnpm dev # app avec HMR, dans un autre terminal (port PORT, 4005 par défaut)
```
Détails, pièges et remise à zéro : concept `data-layer`, `cookbook_local-activitypods-platform`.
## Commandes utiles
```bash
+12 -6
View File
@@ -1,14 +1,20 @@
SEMAPPS_APP_NAME=Festipod
SEMAPPS_APP_DESCRIPTION=Points de rencontre sur les événements publics
SEMAPPS_APP_LANG=en,fr
SEMAPPS_HOME_URL=http://localhost:3003
SEMAPPS_FRONT_URL=http://localhost:4000
SEMAPPS_HOME_URL=http://localhost:3005/
SEMAPPS_FRONT_URL=http://localhost:4005/
SEMAPPS_PORT=3005
SEMAPPS_SHAPE_REPOSITORY_URL=https://shapes.activitypods.org/
SEMAPPS_PORT=3003
# Triple store
SEMAPPS_SPARQL_ENDPOINT=http://localhost:3030/
SEMAPPS_MAIN_DATASET=festipod
SEMAPPS_JENA_USER=admin
SEMAPPS_JENA_PASSWORD=admin
SEMAPPS_REDIS_CACHE_URL=redis://localhost:6390/3
SEMAPPS_QUEUE_SERVICE_URL=redis://localhost:6390/4
SEMAPPS_MAIN_DATASET=festipod
SEMAPPS_AUTH_ACCOUNTS_DATASET_NAME=settings-festipod
# Redis (format: redis://localhost:6379/0) — no cache locally; queue db 15 (the sibling apps use 12, 13, 14)
SEMAPPS_REDIS_CACHE_URL=
SEMAPPS_QUEUE_SERVICE_URL=redis://localhost:6379/15
+1 -1
View File
@@ -1,5 +1,5 @@
// The path the backend is served under, read from its base URL (SEMAPPS_HOME_URL): `/` when the
// backend owns its origin (http://localhost:3003 locally), `/api/` when it shares the front end's
// backend owns its origin (http://localhost:3005 locally), `/api/` when it shares the front end's
// origin (https://<host>/api/, the Coolify layout, where one base URL per environment keeps pull
// request previews on a single host). The proxy in front passes the path through unchanged; SemApps
// mounts every route it adds under the base path, except the `/.well-known` ones, which stay at the
+1 -1
View File
@@ -17,7 +17,7 @@ const registration = require('./registration.service');
const participations = require('./participations.service');
const HOST = 'http://localhost:3000';
const APP = 'http://localhost:4000/as/actor/app';
const APP = 'http://localhost:4005/as/actor/app';
const ALICE = `${HOST}/alice`; // creates the meeting points
const BOB = `${HOST}/bob`; // participates
const CAROL = `${HOST}/carol`; // participates too
+79
View File
@@ -0,0 +1,79 @@
services:
fuseki:
image: semapps/jena-fuseki-webacl
container_name: fuseki
volumes:
- ./data/fuseki:/fuseki:z
ports:
- '3030:3030'
expose:
- '3030'
environment:
ADMIN_PASSWORD: admin
activitypods-backend:
image: activitypods/backend
container_name: activitypods-backend
depends_on:
- fuseki
- redis
restart: always
expose:
- '3000'
volumes:
- ./data/backend/logs:/app/backend/logs:z
- ./data/backend/jwt:/app/backend/jwt:z
- ./data/backend/uploads:/app/backend/uploads:z
environment:
SEMAPPS_JENA_PASSWORD: admin
SEMAPPS_DEFAULT_LOCALE: en
# Allow the Pod backend to access the app backend
# See https://docs.docker.com/network/drivers/host/
network_mode: 'host'
activitypods-frontend:
image: activitypods/frontend
container_name: activitypods-frontend
depends_on:
- activitypods-backend
restart: always
ports:
- '5000:5000'
expose:
- '5000'
environment:
REACT_APP_CONFIG_URL: 'http://localhost:3000/.well-known/config.js'
mailcatcher:
image: dockage/mailcatcher:0.7.1
container_name: mailcatcher
ports:
- '1080:1080'
- '1025:1025'
restart: on-failure
redis:
image: redis
container_name: redis
expose:
- '6379'
ports:
- '6379:6379'
volumes:
- ./data/redis:/data:z
command: ['redis-server', '--appendonly', 'yes']
arena:
image: activitypods/arena
container_name: arena
restart: always
volumes:
- ./docker/arena-dev.json:/opt/arena/src/server/config/index.json:z
depends_on:
- redis
expose:
- '4567'
ports:
- '4567:4567'
environment:
REDIS_HOST: 'redis'
+3
View File
@@ -30,6 +30,9 @@
# disk — remove `databases/<main>`, `databases/<main>Acl`, `databases/<main>Mirror` and
# `configuration/<main>.ttl` under the Fuseki base too, or a dataset created again under
# the same name reopens the old data;
# Fuseki then keeps the closed storage of the deleted dataset: re-creating one under the
# SAME name fails every query with `ClosedException: already closed` until Fuseki restarts
# (the shared one, on the test server) — or set new MAIN_DATASET / SETTINGS_DATASET names;
# 2. empty this stack's Redis (the `redis-data` volume): its cache outlives the datasets and
# would keep serving the old app actor, keys and access needs;
# 3. redeploy.
-29
View File
@@ -1,29 +0,0 @@
services:
frontend:
build:
context: .
dockerfile: Dockerfile
ports:
- "4000:3000"
environment:
- PORT=3000
- NODE_ENV=production
- VITE_BACKEND_URL=${VITE_BACKEND_URL:-http://localhost:3003}
- VITE_BACKEND_CLIENT_ID=${VITE_BACKEND_CLIENT_ID:-}
- FESTIPOD_SHARED_WALLET_PASSWORD=${FESTIPOD_SHARED_WALLET_PASSWORD:-}
- FESTIPOD_SHARED_WALLET_FILE=${FESTIPOD_SHARED_WALLET_FILE:-}
- FESTIPOD_SHARED_WALLET_FILE_BASE64=${FESTIPOD_SHARED_WALLET_FILE_BASE64:-}
- FESTIPOD_AUTO_SEED=${FESTIPOD_AUTO_SEED:-}
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "3003:3003"
env_file:
- path: ./backend/.env
required: false
environment:
- SEMAPPS_HOME_URL=${SEMAPPS_HOME_URL:-http://localhost:3003}
- SEMAPPS_FRONT_URL=${SEMAPPS_FRONT_URL:-http://localhost:4000}
+74
View File
@@ -0,0 +1,74 @@
{
"queues": [
{
"name": "sendMail",
"hostId": "activitypods",
"host": "redis",
"port": 6379,
"db": 1
},
{
"name": "remotePost",
"hostId": "activitypods",
"host": "redis",
"port": 6379,
"db": 1
},
{
"name": "processOutbox",
"hostId": "activitypods",
"host": "redis",
"port": 6379,
"db": 1
},
{
"name": "processInbox",
"hostId": "activitypods",
"host": "redis",
"port": 6379,
"db": 1
},
{
"name": "webhookPost",
"hostId": "activitypods",
"host": "redis",
"port": 6379,
"db": 1
},
{
"name": "remotePost",
"hostId": "festipod",
"host": "redis",
"port": 6379,
"db": 15
},
{
"name": "processOutbox",
"hostId": "festipod",
"host": "redis",
"port": 6379,
"db": 15
},
{
"name": "processInbox",
"hostId": "festipod",
"host": "redis",
"port": 6379,
"db": 15
},
{
"name": "registerListener",
"hostId": "festipod",
"host": "redis",
"port": 6379,
"db": 15
},
{
"name": "timeout",
"hostId": "festipod",
"host": "redis",
"port": 6379,
"db": 15
}
]
}
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "portless festipod bun --hot src/index.ts",
"dev": "bun --hot src/index.ts",
"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",
+2 -2
View File
@@ -20,10 +20,10 @@ import { fetchCollectionMemberIds } from '../src/shared/data/contacts';
const TS = Date.now();
const PASS = 'Test1test';
const POD = 'http://localhost:3000';
const POD = process.env.VITE_POD_PROVIDER_URL || 'http://localhost:3000';
const A_NAME = `diaginv${TS}a`;
const B_NAME = `diaginv${TS}b`;
const config = { podProviderUrl: POD, backendUrl: process.env.BACKEND_URL || 'http://localhost:3003' };
const config = { podProviderUrl: POD, backendUrl: process.env.BACKEND_URL || process.env.VITE_BACKEND_URL || 'http://localhost:3005' };
const CRED_CTX = [
'https://www.w3.org/ns/credentials/v2',
+5 -5
View File
@@ -9,7 +9,7 @@
* runs it end-to-end and fails on any UNEXPECTED ≥400, so the regression is
* caught without a human driving the browser.
*
* ORIGIN. The OIDC redirect_uri is registered for `http://localhost:4000/login`
* ORIGIN. The OIDC redirect_uri is registered for the app origin `/login` (default http://localhost:4005)
* only (the app's FRONT_URL), so the app MUST be served at APP_URL — do not
* serve a private instance on an ephemeral port: the pod rejects the redirect.
*
@@ -18,7 +18,7 @@
* leave the app for the pod, the consent Accept is clicked once stable and the
* page must then leave the consent screen, and the shell counts only with the
* bottom navigation shown and no picker). The previous local loop could report
* "shell reached" while still on `:4000/` — the user never consented, the app
* "shell reached" while still on the app origin `/` — the user never consented, the app
* was never installed. `authenticated` also requires the install to be visible
* on the pod (the user's registered Event container answers 200).
*
@@ -31,10 +31,10 @@ import type { ApodsStackConfig } from '../src/shared/support/activitypodsStack';
import { driveLogin, installedContainer, loginWithRetry, type DriveContext } from './e2e-rights-matrix-support';
const strip = (u: string): string => u.replace(/\/+$/, '');
const BACKEND: string = strip(process.env.BACKEND_URL || 'http://localhost:3003');
const APP: string = strip(process.env.APP_URL || 'http://localhost:4000');
const BACKEND: string = strip(process.env.BACKEND_URL || process.env.VITE_BACKEND_URL || 'http://localhost:3005');
const APP: string = strip(process.env.APP_URL || `http://localhost:${process.env.PORT || 4005}`);
const config: ApodsStackConfig = {
podProviderUrl: strip(process.env.POD_PROVIDER_URL || 'http://localhost:3000'),
podProviderUrl: strip(process.env.POD_PROVIDER_URL || process.env.VITE_POD_PROVIDER_URL || 'http://localhost:3000'),
backendUrl: BACKEND,
};
const POD_FRONT: string = strip(process.env.POD_FRONTEND_URL || 'http://localhost:5000');
+4 -4
View File
@@ -35,7 +35,7 @@
* visibilitychange); its one-minute throttle is untouched.
*
* ORIGIN. As for `e2e-login-flow.ts`: the OIDC redirect_uri is registered for
* `http://localhost:4000/login` only, so the app MUST be served at APP_URL.
* the app origin `/login` (default http://localhost:4005) only, so the app MUST be served at APP_URL.
*
* Exit code 0 only when every phase holds; the JSON report says which did not.
*/
@@ -52,10 +52,10 @@ import {
import { driveLogin, type DriveContext } from './e2e-rights-matrix-support';
const strip = (u: string): string => u.replace(/\/+$/, '');
const BACKEND: string = strip(process.env.BACKEND_URL || 'http://localhost:3003');
const APP: string = strip(process.env.APP_URL || 'http://localhost:4000');
const BACKEND: string = strip(process.env.BACKEND_URL || process.env.VITE_BACKEND_URL || 'http://localhost:3005');
const APP: string = strip(process.env.APP_URL || `http://localhost:${process.env.PORT || 4005}`);
const config: ApodsStackConfig = {
podProviderUrl: strip(process.env.POD_PROVIDER_URL || 'http://localhost:3000'),
podProviderUrl: strip(process.env.POD_PROVIDER_URL || process.env.VITE_POD_PROVIDER_URL || 'http://localhost:3000'),
backendUrl: BACKEND,
};
const POD_FRONT = strip(process.env.POD_FRONTEND_URL || 'http://localhost:5000');
+1 -1
View File
@@ -379,7 +379,7 @@ export interface LoginOutcome { ok: boolean; detail: string }
* still open (a sign-out of the app keeps it: the revocation probe's second
* sign-in), in which case no form is shown and the hops below start at once;
* after the credentials the
* browser comes BACK to the app's OIDC callback (:4000/login?code…), and only
* browser comes BACK to the app's OIDC callback (<app origin>/login?code…), and only
* then — when the app is not yet registered on this pod — does the app send it
* to the pod's consent screen (:5000/authorize), which navigates a second time
* right after it opens. Accept leads back to the app, then to the shell.
+4 -4
View File
@@ -109,7 +109,7 @@
* the forged state itself could not be produced.
*
* ORIGIN. As for `e2e-login-flow.ts`: the OIDC redirect_uri is registered for
* `http://localhost:4000/login` only, so the app MUST be served at APP_URL.
* the app origin `/login` (default http://localhost:4005) only, so the app MUST be served at APP_URL.
*/
import { writeFileSync } from 'node:fs';
@@ -158,11 +158,11 @@ import {
const strip = (u: string): string => u.replace(/\/+$/, '');
const BACKEND = strip(process.env.BACKEND_URL || 'http://localhost:3003');
const APP = strip(process.env.APP_URL || 'http://localhost:4000');
const BACKEND = strip(process.env.BACKEND_URL || process.env.VITE_BACKEND_URL || 'http://localhost:3005');
const APP = strip(process.env.APP_URL || `http://localhost:${process.env.PORT || 4005}`);
const POD_FRONT = strip(process.env.POD_FRONTEND_URL || 'http://localhost:5000');
const config: ApodsStackConfig = {
podProviderUrl: strip(process.env.POD_PROVIDER_URL || 'http://localhost:3000'),
podProviderUrl: strip(process.env.POD_PROVIDER_URL || process.env.VITE_POD_PROVIDER_URL || 'http://localhost:3000'),
backendUrl: BACKEND,
};
+1 -1
View File
@@ -37,7 +37,7 @@
*
* 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
* Terminal 2: pnpm dev # 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)
+3 -3
View File
@@ -76,10 +76,10 @@ const TIMEOUT_REVOCATION_MS = 15 * 60 * 1000; // 15 min (the probe bounds itself
/** The local stack, with the same defaults and env overrides as the e2e scripts. */
const strip = (u: string): string => u.replace(/\/+$/, "");
const POD_PROVIDER = strip(process.env.POD_PROVIDER_URL || "http://localhost:3000");
const POD_PROVIDER = strip(process.env.POD_PROVIDER_URL || process.env.VITE_POD_PROVIDER_URL || 'http://localhost:3000');
const POD_FRONT = strip(process.env.POD_FRONTEND_URL || "http://localhost:5000");
const BACKEND = strip(process.env.BACKEND_URL || "http://localhost:3003");
const APP = strip(process.env.APP_URL || "http://localhost:4000");
const BACKEND = strip(process.env.BACKEND_URL || process.env.VITE_BACKEND_URL || 'http://localhost:3005');
const APP = strip(process.env.APP_URL || `http://localhost:${process.env.PORT || 4005}`);
const FUSEKI = strip(process.env.FUSEKI_URL || "http://localhost:3030");
/** What the browser-driven e2e probes need answering 200 — every piece of the local stack. */
+4 -4
View File
@@ -14,9 +14,9 @@ test('clientId uses explicit backendClientId when set', () => {
});
test('clientId derives the actor uri from backendUrl when backendClientId is empty', () => {
setRuntimeConfig({ backendUrl: 'http://localhost:3003', backendClientId: '' });
setRuntimeConfig({ backendUrl: 'http://localhost:3005', backendClientId: '' });
const cfg = resolveActivityPodsAuthConfig('http://localhost:5000');
expect(cfg.clientId).toBe('http://localhost:3003/as/actor/app');
expect(cfg.clientId).toBe('http://localhost:3005/as/actor/app');
expect(cfg.redirectUri).toBe('http://localhost:5000/login');
});
@@ -27,9 +27,9 @@ test('clientId falls back to the origin actor uri when nothing is configured', (
});
test('clientId does not double the slash on a trailing-slash backendUrl', () => {
setRuntimeConfig({ backendUrl: 'http://localhost:3003/' });
setRuntimeConfig({ backendUrl: 'http://localhost:3005/' });
const cfg = resolveActivityPodsAuthConfig('http://localhost:5000');
expect(cfg.clientId).toBe('http://localhost:3003/as/actor/app');
expect(cfg.clientId).toBe('http://localhost:3005/as/actor/app');
});
test('podProviderUrl is passed through when set', () => {
+1 -1
View File
@@ -1,7 +1,7 @@
import { serve } from "bun";
import index from "./index.html";
const port = process.env.PORT ? parseInt(process.env.PORT) : 4000;
const port = process.env.PORT ? parseInt(process.env.PORT) : 4005;
// Strict base64 check (not a mere `Buffer.from` attempt, which silently drops invalid
// characters instead of failing): reject anything that is not a well-formed base64 body
@@ -10,7 +10,7 @@ Fonctionnalité: Smoke e2e de la pile ActivityPods locale
# aucun broker. Elle n'existe que si la pile locale tourne — son mode d'emploi
# reproductible est le concept `data-layer`,
# `cookbook_local-activitypods-platform.md` (pod provider :3000, backend Festipod
# :3003, redis remappé :6390). Le chargement de l'app reprend le motif `@e2e` du
# :3005 par défaut). Le chargement de l'app reprend le motif `@e2e` du
# repo (serveur réel + Playwright), pas le wallet partagé.
Scénario: La pile locale est joignable et le backend sert son application
+1 -1
View File
@@ -255,7 +255,7 @@ let RefineDataProvider: (props: { children: ReactNode }) => ReactNode;
let root: ReturnType<typeof createRoot> | undefined;
beforeAll(async () => {
const window = new Window({ url: 'http://localhost:4000/' });
const window = new Window({ url: 'http://localhost:4005/' });
const g = globalThis as Record<string, unknown>;
for (const [name, value] of Object.entries({ window, document: window.document, navigator: window.navigator })) {
if (g[name] !== undefined) continue;
@@ -60,7 +60,7 @@ function Probe({ uris, options }: { uris: readonly string[]; options: PublicReco
}
beforeAll(async () => {
const window = new Window({ url: 'http://localhost:4000/' });
const window = new Window({ url: 'http://localhost:4005/' });
const g = globalThis as Record<string, unknown>;
for (const [name, value] of Object.entries({ window, document: window.document, navigator: window.navigator })) {
if (g[name] !== undefined) continue;
+1 -1
View File
@@ -1028,7 +1028,7 @@ export const parsedFeatures: ParsedFeature[] = [
}
],
"filePath": "src/modules/event/features/e2e-activitypods.feature",
"rawContent": "# language: fr\n@EVENT @priority-1 @e2e-activitypods\nFonctionnalité: Smoke e2e de la pile ActivityPods locale\n En tant que développeur validant le branchement ActivityPods\n Je pilote la pile locale réelle (pod provider + backend Festipod + app servie)\n Afin de prouver que les flux du modèle passent réellement par le Pod.\n\n # PÉRIMÈTRE. Cette couche est DISTINCTE des suites `@data`/`@e2e`/`@multibrowser`\n # suspendues avec le workstream NextGraph : elle ne signe aucun wallet et ne monte\n # aucun broker. Elle n'existe que si la pile locale tourne — son mode d'emploi\n # reproductible est le concept `data-layer`,\n # `cookbook_local-activitypods-platform.md` (pod provider :3000, backend Festipod\n # :3003, redis remappé :6390). Le chargement de l'app reprend le motif `@e2e` du\n # repo (serveur réel + Playwright), pas le wallet partagé.\n\n Scénario: La pile locale est joignable et le backend sert son application\n Alors le pod provider local répond sur son endpoint OpenID\n Et le backend Festipod sert la description de son application\n\n @browser\n Scénario: L'application démarre en variante ActivityPods et propose la connexion au Pod\n Quand l'application Festipod est chargée depuis la pile locale\n Alors l'entrée de connexion au Pod est proposée\n Et aucune erreur runtime n'a été émise au chargement\n\n Scénario: Un événement et un point de rencontre rattaché par as:context sont créés sur le Pod\n Étant donné une session Pod locale pour \"alice\" avec le mot de passe \"Test1test\"\n Quand je crée un événement \"Concert smoke\" et un point de rencontre \"Rdv entrée\" rattaché par as:context\n Alors la lecture du point de rencontre expose son lien as:context vers l'événement\n Et l'événement et le point de rencontre partagent le conteneur as:Event\n\n # L'app s'installe sur le Pod (consentement de l'agent d'autorisation, ou pour un\n # scénario sans navigateur l'endpoint d'enregistrement — voir l'étape\n # « installée sur le Pod »). Une fois installée, l'activité `as:Join` postée sur\n # l'outbox de l'utilisateur EST délivrée au backend, qui tient la collection\n # `apods:attendees` du point de rencontre. Le point de rencontre est créé dans le\n # conteneur enregistré (`as/event`) ; sa collection `apods:attendees` est attachée\n # par le backend lui-même, à la création — le scénario la lit, il ne la crée pas.\n Scénario: Rejoindre puis quitter un point de rencontre met à jour la collection apods:attendees\n Étant donné une session Pod locale pour \"alice\" avec le mot de passe \"Test1test\"\n Et l'application Festipod est installée sur le Pod de la session\n Et un point de rencontre \"Rdv entrée\" rattaché à un événement, avec sa collection apods:attendees\n Quand la session rejoint le point de rencontre\n Alors la collection apods:attendees contient la session\n Quand la session quitte le point de rencontre\n Alors la collection apods:attendees ne contient plus la session\n\n # Dépend du scénario précédent pour la mise en place, mais lit la collection\n # elle-même : le compteur est le total de la collection (jamais les profils\n # résolus) et une référence non résolue reste comptée et rendue anonyme.\n Scénario: Le compteur et la liste des participants tiennent une référence non résolue\n Étant donné une session Pod locale pour \"alice\" avec le mot de passe \"Test1test\"\n Et l'application Festipod est installée sur le Pod de la session\n Et un point de rencontre \"Rdv entrée\" rattaché à un événement, avec sa collection apods:attendees\n Quand la session rejoint le point de rencontre\n Alors le compteur du point de rencontre compte cette inscription\n Et la liste des participants rend ce participant comme anonyme\n",
"rawContent": "# language: fr\n@EVENT @priority-1 @e2e-activitypods\nFonctionnalité: Smoke e2e de la pile ActivityPods locale\n En tant que développeur validant le branchement ActivityPods\n Je pilote la pile locale réelle (pod provider + backend Festipod + app servie)\n Afin de prouver que les flux du modèle passent réellement par le Pod.\n\n # PÉRIMÈTRE. Cette couche est DISTINCTE des suites `@data`/`@e2e`/`@multibrowser`\n # suspendues avec le workstream NextGraph : elle ne signe aucun wallet et ne monte\n # aucun broker. Elle n'existe que si la pile locale tourne — son mode d'emploi\n # reproductible est le concept `data-layer`,\n # `cookbook_local-activitypods-platform.md` (pod provider :3000, backend Festipod\n # :3005 par défaut). Le chargement de l'app reprend le motif `@e2e` du\n # repo (serveur réel + Playwright), pas le wallet partagé.\n\n Scénario: La pile locale est joignable et le backend sert son application\n Alors le pod provider local répond sur son endpoint OpenID\n Et le backend Festipod sert la description de son application\n\n @browser\n Scénario: L'application démarre en variante ActivityPods et propose la connexion au Pod\n Quand l'application Festipod est chargée depuis la pile locale\n Alors l'entrée de connexion au Pod est proposée\n Et aucune erreur runtime n'a été émise au chargement\n\n Scénario: Un événement et un point de rencontre rattaché par as:context sont créés sur le Pod\n Étant donné une session Pod locale pour \"alice\" avec le mot de passe \"Test1test\"\n Quand je crée un événement \"Concert smoke\" et un point de rencontre \"Rdv entrée\" rattaché par as:context\n Alors la lecture du point de rencontre expose son lien as:context vers l'événement\n Et l'événement et le point de rencontre partagent le conteneur as:Event\n\n # L'app s'installe sur le Pod (consentement de l'agent d'autorisation, ou pour un\n # scénario sans navigateur l'endpoint d'enregistrement — voir l'étape\n # « installée sur le Pod »). Une fois installée, l'activité `as:Join` postée sur\n # l'outbox de l'utilisateur, sans destinataire, est traitée par le backend à\n # l'émission : il inscrit dans la collection `apods:attendees` du point de\n # rencontre l'URN de membre dérivée du secret de participation du profil\n # (écrit avant le Join, comme le fait l'app) — jamais le WebID\n # (decision_2026-10-29_participation-privacy-mechanism). Le point de rencontre est créé dans le\n # conteneur enregistré (`as/event`) ; sa collection `apods:attendees` est attachée\n # par le backend lui-même, à la création — le scénario la lit, il ne la crée pas.\n Scénario: Rejoindre puis quitter un point de rencontre met à jour la collection apods:attendees\n Étant donné une session Pod locale pour \"alice\" avec le mot de passe \"Test1test\"\n Et l'application Festipod est installée sur le Pod de la session\n Et un point de rencontre \"Rdv entrée\" rattaché à un événement, avec sa collection apods:attendees\n Quand la session rejoint le point de rencontre\n Alors la collection apods:attendees contient la session\n Quand la session quitte le point de rencontre\n Alors la collection apods:attendees ne contient plus la session\n\n # Dépend du scénario précédent pour la mise en place, mais lit la collection\n # elle-même : le compteur est le total de la collection (jamais les profils\n # résolus) et une URN de membre, que rien ne résout, reste comptée et rendue\n # anonyme.\n Scénario: Le compteur et la liste des participants tiennent une référence non résolue\n Étant donné une session Pod locale pour \"alice\" avec le mot de passe \"Test1test\"\n Et l'application Festipod est installée sur le Pod de la session\n Et un point de rencontre \"Rdv entrée\" rattaché à un événement, avec sa collection apods:attendees\n Quand la session rejoint le point de rencontre\n Alors le compteur du point de rencontre compte cette inscription\n Et la liste des participants rend ce participant comme anonyme\n",
"screenIds": []
},
{
+2 -2
View File
@@ -14,7 +14,7 @@ import { createHash, randomBytes } from 'node:crypto';
*
* THE LOCAL STACK. Its reproducible setup is the concept note
* `.project/concepts/data-layer/cookbook_local-activitypods-platform.md`
* (pod provider on :3000, Festipod backend on :3003, redis remapped to :6390).
* (pod provider on :3000, Festipod backend on :3005 by default, redis remapped to :6390).
* The values come from the same environment variables the app and the backend
* read, with the cookbook's defaults as fallback.
*
@@ -42,7 +42,7 @@ function stripTrailingSlash(url: string): string {
export function apodsStackConfig(): ApodsStackConfig {
return {
podProviderUrl: stripTrailingSlash(process.env.VITE_POD_PROVIDER_URL?.trim() || 'http://localhost:3000'),
backendUrl: stripTrailingSlash(process.env.VITE_BACKEND_URL?.trim() || 'http://localhost:3003'),
backendUrl: stripTrailingSlash(process.env.VITE_BACKEND_URL?.trim() || 'http://localhost:3005'),
};
}