Ng eventually #1
@@ -1,7 +0,0 @@
|
||||
# Doc-debt — app-architecture
|
||||
|
||||
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
|
||||
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
|
||||
|
||||
## Raw markers (consolidate into blocks, then delete)
|
||||
- TOUCHED src/app/frontend.tsx @2026-08-16 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
@@ -20,6 +20,6 @@ The fix: the session starts unconditionally, in the iframe and standalone alike,
|
||||
|
||||
**No test exercises this path.** `@data` runs the harness inside the broker iframe; `@e2e` drives the real app inside the broker iframe too. The standalone top-level boot — the one a developer uses every day with `bun run dev`, and the one a first-time visitor hits — is covered by nothing.
|
||||
|
||||
So: a change to `AuthGate`, to `NextGraphProvider`, or to what `configure()` receives can break the app's entry completely while every suite stays green. If you touch any of them, **open the app standalone yourself** before believing the tests.
|
||||
So: a change to `AuthGate`, to `NextGraphProvider`, or to what `configure()` receives can break the app's entry completely while every suite stays green. If you touch any of them, **open the app yourself** before believing the tests — and drive a whole flow rather than looking at the page, which is what a throwaway probe is for (`bdd-testing` → [[cookbook_live-probe]]).
|
||||
|
||||
Two related pieces: [[caveat_first-time-entry-untested]] (the wallet-import journey, same blind spot seen from the user's side) and [[caveat_shared-wallet-global-before-gate-import]] (a missing wallet password now makes `ensureIdentity()` throw, which at least fails loudly).
|
||||
|
||||
@@ -39,6 +39,6 @@ ThemeProvider
|
||||
|---|---|
|
||||
| `src/index.ts` | `Bun.serve()` — HTTP server, serves `index.html` + the cucumber report |
|
||||
| `src/index.html` | HTML entry point, loads `src/app/frontend.tsx` |
|
||||
| `src/app/frontend.tsx` | React root, renders `<App />` |
|
||||
| `src/app/frontend.tsx` | React root — pulls the runtime config, sets the wallet global, **then** dynamically imports and renders `<App />`; the order is the point (`tech-stack` → [[knowledge_build-pipeline]], `app-security` → [[caveat_shared-wallet-global-before-gate-import]]) |
|
||||
|
||||
The build and the bundler (Bun + Tailwind, alias `@/* → ./src/*`) are documented in the `tech-stack` concept.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: caveat
|
||||
summary: The wallet password is captured at the EVALUATION of src/shared/utils/sharedWallet.ts; a value set after that first import is never re-read — a missing one used to yield AccessGateScreen's error block, now it makes ensureIdentity() throw and the app render nothing at all, silently
|
||||
last_checked: 2026-08-10
|
||||
summary: The wallet password is captured at the EVALUATION of src/shared/utils/sharedWallet.ts — a value set after that first import is never re-read, and the fetch that sets it must never be gated on NODE_ENV (production serves from src/). Missing it: ensureIdentity() throws, AuthGate shows the reason.
|
||||
last_checked: 2026-08-16
|
||||
---
|
||||
|
||||
# Pitfall: set the wallet-password global BEFORE the module is first imported
|
||||
@@ -15,8 +15,8 @@ The contract requires a deployment to **serve a wallet file and pass its URL and
|
||||
## Impact — if I touch X, Y breaks
|
||||
|
||||
- **Static import = trap.** A static `import` reaching `ngSession.ts` (hence `sharedWallet.ts`) from an entry point that sets the global itself is **hoisted above the assignment** → empty password → the failure mode above, with no JS error at the import site to signal it. The remedy is a **dynamic import** (`await import(...)`) executed after setting the global.
|
||||
- **The real entry point that must get this right**: the frontend served from `src/` (`src/app/frontend.tsx` fetches `/festipod-config.json`, sets the global, then imports `App` dynamically — mechanics in `tech-stack` → [[knowledge_build-pipeline]]). A bundle produced by `build.ts` is **not** concerned: there the value is inlined by `define`.
|
||||
- **The real entry point that must get this right**: the frontend served from `src/` (`src/app/frontend.tsx` fetches `/festipod-config.json`, sets the global, then imports `App` dynamically — mechanics in `tech-stack` → [[knowledge_build-pipeline]]). That fetch may be skipped on **one** condition: the global is already set, which only a `build.ts` bundle's `define` does. **Never on an `NODE_ENV` test** — this project's production serves from `src/` exactly like dev, so gating the fetch on "production" is what removed the wallet from the deployed app and left it unable to sign anybody in.
|
||||
- **`@ui` reaches the module too, but harmlessly today.** `screens/index.ts` eagerly imports every screen including `SettingsScreen`, which imports `ngSession.ts` — so any `@ui` test already evaluates `sharedWallet.ts` with the global unset. This does not currently break anything because no `@ui` path calls `ensureIdentity()` (`renderScreen()` bypasses `AuthGate`/`NextGraphProvider` entirely); see `bdd-testing` → [[knowledge_ui-layer]] for the detail and for what would make it stop being harmless.
|
||||
- **Operations**: a server without `FESTIPOD_SHARED_WALLET_PASSWORD` now fails **silently** (blank page, console-only) rather than with a screen saying so — worth knowing when diagnosing "the app shows nothing."
|
||||
- **Operations**: a server without `FESTIPOD_SHARED_WALLET_PASSWORD` cannot sign anyone in, and it **says so** — `ensureIdentity()` rejects and `AuthGate` renders its named error panel carrying the reason. Verified live on a rejected sign-in: a refusal shows the reason, not a blank page (`bdd-testing` → [[cookbook_live-probe]]). The failure mode still worth fearing is the **silent** one: a promise that never settles either way renders nothing at all and logs nothing — `app-architecture` → [[caveat_boot-unverified-outside-broker]].
|
||||
|
||||
**Verified (2026-08-10)**: capture at evaluation time in `src/shared/utils/sharedWallet.ts`; the `sharedWallet: hasSharedWallet() ? {...} : undefined` branch in `ngSession.ts`'s `configure()` call; the `throw` in `ensureIdentity()` when no `sharedWallet` config is present; `AuthGate`'s `.catch(err => console.error(...))` with no fallback UI.
|
||||
|
||||
@@ -39,3 +39,4 @@ BDD tests written in **Cucumber/Gherkin in French** (`Etant donné`, `Quand`, `A
|
||||
- [[decision_2026-03-12_headless-wallet-creation]] — why the test wallet is created through a headless UI
|
||||
- [[caveat_source-grep-vestiges]] — leftovers from the "source analysis" era in `world.ts`
|
||||
- [[cookbook_add-scenario]] — adding a scenario/step (layers, `evaluate` serialization pitfall, `@wip`)
|
||||
- [[cookbook_live-probe]] — verifying a flow for real when the suite cannot answer: a throwaway Playwright probe on the real app, what it must collect, and why its findings must be written down the same day
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: cookbook
|
||||
summary: How to verify a flow for real when the suite cannot answer — a throwaway Playwright probe that boots the REAL app in a real browser against the real broker and drives the UI as a user does; what it must collect, and why its findings must land in doctrine the same day.
|
||||
last_checked: 2026-08-16
|
||||
---
|
||||
|
||||
# Driving the real app with a throwaway probe
|
||||
|
||||
A **probe** is a one-off Playwright script, outside Cucumber — no World, no hooks, no fixtures — that boots the **real app** in a real browser against the **real broker** and drives its interface the way a user does. You write it, you run it, you read it, you delete it.
|
||||
|
||||
## When to reach for one
|
||||
|
||||
Before believing a flow works. The create-and-participate flow had been declared *correct by construction* on typecheck, build and reading; the first probe ever run against it found **three defects** none of those could see — two still open ([[bug_signup-breaks-the-next-connection]], [[bug_participant-count-stays-at-zero]] in `data-layer`) and one shipped as a fix.
|
||||
|
||||
Reach for it when the suite cannot answer the question: the `@data` run dies silently from around its sixth scenario ([[caveat_wallet-bloat-hang]]), its scenarios have no fixtures ([[caveat_data-suite-has-no-fixtures]]), and entry paths are covered by nothing ([[caveat_first-time-entry-untested]], `app-architecture` → [[caveat_boot-unverified-outside-broker]]).
|
||||
|
||||
## Method
|
||||
|
||||
1. **Drive the app's own interface, never a bridge.** A probe that calls into the data context proves the data context. The whole point is the collaboration between the layers, so the only inputs are the ones a user gives — clicks, typing, waiting — and the only outputs are the ones a user sees.
|
||||
2. **Reuse the boot the `@e2e` layer already documents** ([[knowledge_e2e-layer]]) rather than inventing one: the app server on its own port, the broker round-trip, the app in its iframe. Do not build a second way in.
|
||||
3. **Collect `pageerror` and `console` from the first navigation.** The findings that matter surface as a rejection raised *inside a layer you never called* — invisible on screen except as a panel saying something failed.
|
||||
4. **Time the steps you assert on.** "The toast landed after the write" and "1.8 s" are two different findings; the second is what makes a later regression legible.
|
||||
5. **Keep watching after the confirmation, then reconnect.** A step that reports honestly can still leave the flow wrong. Give the state a real interval (minutes, not a tick), then come back through a fresh load — most of what a probe finds lives after the point where a test would have asserted green.
|
||||
6. **Say what state you started from.** A **brand-new origin with a brand-new identity** is what separates a real defect from accumulated wallet state, and a finding reported without it is not yet a finding. Report the run count too (*"3 of 3"*).
|
||||
|
||||
## What a probe is not
|
||||
|
||||
It is **not a regression guard**: nothing re-runs it, and a deleted script protects nothing. Its whole value is converted at the end of the run, into doctrine or a `bug_` leaf, the same day — a probe run that is not written down bought nothing. Recording an observation, mark **VERIFIED** (seen, with the run count) apart from **INFERRED** (the explanation you reached for); a real symptom does not certify its diagnosis.
|
||||
|
||||
> **The lesson that pays for the method: honest steps do not add up to an honest flow.** Every step of the sign-up reports truthfully — the mutation rejects rather than lying, the confirmation follows the write — and the flow as a whole still announces a success it does not obtain. No layer can see that from inside itself; only exercising the whole thing end to end shows it.
|
||||
@@ -27,6 +27,11 @@ How Festipod **persists its data** through NextGraph (P2P, local-first, end-to-e
|
||||
- [[rule_document-per-entity]] — every entity gets **its own document** (per scope), never one at store level; access is granted per document, so this is what makes isolation possible
|
||||
- [[rule_app-uses-sdk-surface-only]] — the pulled contract is the only reference; a gap in it is raised with the provider, never worked around here
|
||||
|
||||
## Open defects — the sign-up flow does not deliver
|
||||
|
||||
- [[bug_signup-breaks-the-next-connection]] — after a sign-up, the next connection fails; a **provider-side gap**, raised with the provider, nothing to work around here
|
||||
- [[bug_participant-count-stays-at-zero]] — the count never moves in the session that signs up; whether it converges later is **unknown**, and unmeasurable while the bug above holds
|
||||
|
||||
## Pitfalls (read before touching deletions / event fields)
|
||||
|
||||
- [[caveat_participation-deletion]] — withdrawal must be **authoritative** and must not come back
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: bug
|
||||
severity: major
|
||||
opened: 2026-08-16
|
||||
last_checked: 2026-08-16
|
||||
summary: After a sign-up the count stays at 0 for the rest of the session while the button reads « ✓ Je participe » — observed with the event's owner (the counter's only writer) present and connected. Whether it converges at the next connection is UNKNOWN, not known-good.
|
||||
---
|
||||
|
||||
# The participant count does not converge in the same session
|
||||
|
||||
## What happens, VERIFIED
|
||||
|
||||
In the create-and-participate flow — declare an event, sign up to it — the event's `participantCount` **stays at 0 for the rest of the session** while the button reads « ✓ Je participe ». **VERIFIED 2 runs out of 2**, the count still 0 **120 s** and **75 s** after the sign-up.
|
||||
|
||||
The count starting at 0 on creation is correct and is not the defect ([[knowledge_context-internals]] §participantCount: no host baseline). The defect is that it never moves afterwards.
|
||||
|
||||
**The "owner offline" explanation does not apply.** In this flow the signer **is** the event's owner, so the counter's only writer is present, connected, and watching the inbox it deposited into. Eventual delivery to an absent owner explains nothing here.
|
||||
|
||||
## What is NOT established
|
||||
|
||||
**Whether the count converges at the next connection is UNKNOWN** — it could not be measured, because [[bug_signup-breaks-the-next-connection]] makes the next connection fail. Do not write it down as converging, and do not treat "it will settle on reload" as a known behaviour: nobody has seen a reload.
|
||||
|
||||
**Which side is at fault is also open** — never written, or written and not re-read. The pair of measurement points laid down for exactly this question (the owner's materializer logging `participantCount` before → after its write, and the display read logging the value as exposed to the render — [[knowledge_context-internals]] §logging) is where a diagnosis starts; the probe read the value as displayed and did not settle the pair.
|
||||
|
||||
## Reproduce
|
||||
|
||||
1. Connect, declare an event (the count shows 0 — correct).
|
||||
2. Sign up to it; the button reaches « ✓ Je participe ».
|
||||
3. Stay on the page and watch the count for a couple of minutes. It stays at 0.
|
||||
|
||||
Method: `bdd-testing` → [[cookbook_live-probe]]. Watching *after* the confirmation, over a real interval, is what makes this visible at all — every individual step reports success.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
type: bug
|
||||
severity: major
|
||||
opened: 2026-08-16
|
||||
last_checked: 2026-08-16
|
||||
summary: After a sign-up, the NEXT connection fails — ensureIdentity() rejects inside the data layer's own inbox processing and AuthGate renders « Connexion impossible ». A provider-side gap, raised with the provider; the app names no document to any call and has nothing to fix here.
|
||||
---
|
||||
|
||||
# Signing up locks the account out of its next connection
|
||||
|
||||
## What happens, VERIFIED
|
||||
|
||||
Drive the create-and-participate flow (declare an event, sign up to it), then reconnect. `ensureIdentity()` **rejects**, and the app renders its named error panel, « Connexion impossible ». The rejection comes from **inside the data layer's own inbox processing** — a call the app never made.
|
||||
|
||||
The message, verbatim:
|
||||
|
||||
```
|
||||
docs.sparqlQuery: refused — the connected user does not hold this document's cap.
|
||||
Naming a document does not grant access to it
|
||||
```
|
||||
|
||||
**VERIFIED 3 runs out of 3**, including one on a **brand-new origin with a brand-new identity** — so this is not accumulated state from an old wallet ([[caveat_wallet-bloat-hang]] in `bdd-testing` is a different phenomenon and does not explain it).
|
||||
|
||||
**VERIFIED**: the symptom and the sequence — a sign-up, then a failing reconnection.
|
||||
**INFERRED**: that the deposit is what causes it. The sign-up is the only thing between a connection that works and the next one that does not, but nothing observed names the failing document.
|
||||
|
||||
## Why there is nothing to work around here
|
||||
|
||||
The app deposits through the **published** `inbox.postToDocument(doc, …)` ([[contract_polyfill-surface]]) and **names no document to any call** that could refuse one — it holds no inbox address at all ([[caveat_event-fields-not-persisted]] on why the vestigial `inbox` field must stay unused). The refusal is raised by a query the data layer issues for itself while draining what it was given.
|
||||
|
||||
So this is a **provider-side gap**, raised with the provider — not an app-side problem with a clever fix ([[rule_app-uses-sdk-surface-only]] §2: a workaround is a doctrine violation even when it works). There is no app-side recovery either: a rejected `ensureIdentity()` is the contract's own instruction *not to render past it*, because a session that failed looks exactly like an account that owns nothing.
|
||||
|
||||
## Reproduce
|
||||
|
||||
1. Connect, declare an event, sign up to it (the button reaches « ✓ Je participe »).
|
||||
2. Reconnect — a new page load through the broker, same identity.
|
||||
3. The barrier resolves, then the app shows « Connexion impossible » with the message above in the console.
|
||||
|
||||
A **fresh origin and a fresh identity** is what separates this defect from accumulated wallet state; run it that way before reporting anything new about it. Method: `bdd-testing` → [[cookbook_live-probe]].
|
||||
|
||||
## Blast radius
|
||||
|
||||
Every account is one sign-up away from being locked out, and the lock-out is permanent for that identity as far as anything observed goes. It also **blocks measurement of other defects**: whether the participant count converges across connections cannot be established while this holds — [[bug_participant-count-stays-at-zero]].
|
||||
@@ -70,8 +70,10 @@ The counter is **not** incremented by whoever joins: only a document's owner wri
|
||||
|
||||
- A participant writes their **own** participation document (protected), then **deposits** a marker into the event's inbox (`depositRegistration` / `depositLeave`, `src/shared/data/registration.ts`).
|
||||
- The event **owner's** session watches the inboxes of the events it owns (`inbox.watch`, no polling) and **recomputes** `participantCount` on its own event document. It is the counter's only writer, and it reads through `inbox.readSynced` — the synced view — not `inbox.read`.
|
||||
- **Derived, not incremented**: `materializeAttendance` computes the set of distinct active sign-ups (deposits deduped by `uid`, minus those cancelled). `participantCount = |active set|`. There is **no host baseline** — an event has no host, the declarer is not required to attend, so the counter starts at **0** on creation and moves only on real sign-ups. Because it is a pure function of the inbox, a replay converges: no double count, no phantom decrement. The write is guarded so it only fires on a genuine change.
|
||||
- **Derived, not incremented**: `materializeAttendance` computes the set of distinct active sign-ups (deposits deduped by `uid`, minus those cancelled). `participantCount = |active set|`. There is **no host baseline** — an event has no host, the declarer is not required to attend, so the counter starts at **0** on creation and moves only on real sign-ups. Being a pure function of the inbox, a replay is *designed* to converge: no double count, no phantom decrement. The write is guarded so it only fires on a genuine change.
|
||||
- **Owner offline = eventual.** While the owner is disconnected the count does not move for anyone else; nothing is lost. The materializer fires directly on connection, not only on a push, and it never locks in a premature 0.
|
||||
|
||||
> ⚠️ **This section describes the design, and the design is not what a live run does.** Driven end to end with the owner present and connected — the signer *being* the owner — the count stayed at **0** for the rest of the session after a sign-up. Read the convergence properties above as intent to be re-established, not as observed behaviour: [[bug_participant-count-stays-at-zero]].
|
||||
- The counter is an **aggregate**, not the list of named participants — `getEventParticipants` is governed by what the protected scope hands back.
|
||||
|
||||
Which event a deposit belongs to is matched on the **canonical id-form** — see [[knowledge_write-rights-are-ownership]] §Matching, which governs every event-id comparison in this file.
|
||||
|
||||
@@ -26,3 +26,5 @@ The app has **two modes**, both consumed through the `useFestipodData()` hook:
|
||||
- `error` → `LocalDataProvider` with the seed (graceful fallback)
|
||||
|
||||
> Mutations are **genuinely persisted** in connected mode: `joinEvent` writes a Participation into its own document and **deposits** into the event's inbox (the deposit is the delivery — no notification is written for the host), `leaveEvent` deletes authoritatively (see [[caveat_participation-deletion]]). Both **reject** rather than returning quietly when they cannot write, and the screen's confirmation follows the write. In local/demo mode they are **no-ops that still show a success toast** — see [[knowledge_context-internals]].
|
||||
>
|
||||
> **Per-call honesty is not flow-level honesty.** Every one of those calls tells the truth about itself, and the sign-up flow driven end to end still fails — [[bug_participant-count-stays-at-zero]] and [[bug_signup-breaks-the-next-connection]]. Do not read the paragraph above as "signing up works".
|
||||
|
||||
@@ -15,7 +15,7 @@ summary: What is implemented today (event + meeting point lifecycle, profiles, c
|
||||
- User profile, profile update, profile sharing
|
||||
- Friends list (connections), another user's profile
|
||||
|
||||
> Signing up to / withdrawing from a meeting point is **genuinely wired** on the data side: `joinEvent` persists a Participation and deposits into the event's inbox, where its owner reads it; `leaveEvent` deletes the Participation authoritatively (concept `data-layer`, [[caveat_participation-deletion]]). Neither succeeds in silence — they reject rather than returning quietly, and the confirmation the user sees follows the write. Public discovery — a user seeing another user's public event — works too.
|
||||
> **Signing up is wired step by step, and the flow as a whole does not deliver.** Each step is honest: `joinEvent` persists a Participation and deposits into the event's inbox, where its owner reads it; `leaveEvent` deletes the Participation authoritatively (concept `data-layer`, [[caveat_participation-deletion]]); neither succeeds in silence, and the confirmation the user sees follows the write. Driven end to end in a real browser, the sign-up nonetheless **announces a success it does not obtain**: the user is told they participate, the count never moves ([[bug_participant-count-stays-at-zero]]), and their next connection fails ([[bug_signup-breaks-the-next-connection]]). Honest steps do not add up to an honest flow, and nothing short of exercising the whole thing shows it (concept `bdd-testing`, [[cookbook_live-probe]]). Treat the bullet above as *screens reachable*, not as a working journey. Public discovery — a user seeing another user's public event — works too.
|
||||
|
||||
> **Updating an event is reserved to its declarer**, and the interface says so rather than discovering it late: the edit route is decided by ownership, and the confirmation follows the write instead of preceding it (concept `app-architecture`, [[knowledge_screen-pattern]]). Owner-only is not a policy choice here — it is the only reading the data model allows ([[knowledge_data-scopes-and-discovery]]).
|
||||
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Dev runs on bun --hot, prod builds through build.ts (Bun bundler + Tailwind plugin) into dist/, path alias @/* → ./src/*
|
||||
last_checked: 2026-08-10
|
||||
summary: Three run paths — dev AND production both serve from src/ (bun --hot / bun run start), while bun run build produces a dist/ that nothing serves; NODE_ENV therefore never means "I am a bundle"; path alias @/* → ./src/*
|
||||
last_checked: 2026-08-16
|
||||
---
|
||||
|
||||
# Build pipeline
|
||||
|
||||
- **Dev**: `bun --hot src/index.ts` (through `bun run dev`) — HMR, port 3000.
|
||||
- **Prod**: `bun run build` → `build.ts` (Bun bundler + Tailwind plugin) → `dist/`.
|
||||
## Three paths, and only two of them ever run
|
||||
|
||||
| Path | Command | What is served |
|
||||
|---|---|---|
|
||||
| **Dev** | `bun run dev` → `bun --hot src/index.ts` | **`src/`** — HMR, port 3000 |
|
||||
| **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** |
|
||||
|
||||
> ⚠️ **`dist/` has no consumer, and `NODE_ENV=production` does not mean "built".** The container copies the sources and runs `bun run start`, serving from `src/` exactly as dev does ([[knowledge_deployment]]) — **nothing ever serves `dist/`**, here or anywhere else. So any code that branches on `NODE_ENV` to answer *"am I a bundle?"* is wrong in the one place it matters: in production the answer is **no**. That inference shipped once, on the runtime-config fetch below, and the deployed app could sign nobody in. Ask the artifact you care about, never the environment.
|
||||
|
||||
- **Path alias**: `@/* → ./src/*` (declared in `tsconfig.json`, resolved relative to that file — `paths` has needed no `baseUrl` since TS 4.4).
|
||||
|
||||
> ⚠️ **Never put `baseUrl` back in `tsconfig.json`.** TypeScript 6 reports it as an **error that aborts the whole compilation**, and the failure is silent where it hurts: `tsc --noEmit` then exits **0 having checked nothing**, so the typecheck gate goes green over any amount of broken code. A green typecheck is only meaningful if `tsc` actually ran — treat an instant, output-free `tsc` as a red flag, not a fast pass.
|
||||
@@ -23,7 +31,11 @@ The server serves `src/index.html`, which loads `src/app/frontend.tsx` (see `app
|
||||
|
||||
`build.ts` injects **compile-time globals** through `define`: `__FESTIPOD_SHARED_WALLET_PASSWORD__` from `FESTIPOD_SHARED_WALLET_PASSWORD`, and `__FESTIPOD_AUTO_SEED__` from `FESTIPOD_AUTO_SEED` — the dev auto-seed, OFF when absent. **Pitfall**: the `src/index.ts` server (used by `bun run dev` AND `bun run start`) bundles `index.html` through Bun's HTML import, which **applies no `define`** — neither `bun --define` nor `process.env` propagates there (verified). So an environment variable passed to `bun run dev` never reaches the frontend bundle along that path.
|
||||
|
||||
For those paths served from `src/`, the configuration therefore goes through the **runtime**: `src/index.ts` exposes `/festipod-config.json` (read from the environment), and the `src/app/frontend.tsx` entry **fetches it first**, sets the global, **then imports the app dynamically** (`await import('./App')`) — so that `sharedWallet.ts` reads the value when it is evaluated. In a `build.ts` bundle the value is already inlined by `define`, so the fetch is skipped (`NODE_ENV === 'production'`). Practical consequence: to exercise the "shared wallet" flow in dev **end to end** (download plus a working import), pass the REAL password of the e2e wallet **and** the file — the password shown on screen must match the imported `.ngw`, otherwise the import fails (a dummy value such as `1` merely makes the screen appear):
|
||||
Everything served from `src/` — **dev and production alike** — therefore takes its configuration from the **runtime**: `src/index.ts` exposes `/festipod-config.json` (read from the environment), and the `src/app/frontend.tsx` entry **fetches it first**, sets the global, **then imports the app dynamically** (`await import('./App')`) — so that `sharedWallet.ts` reads the value when it is evaluated.
|
||||
|
||||
**The fetch is skipped on one condition only: the global is already set** (which is what a `build.ts` bundle's `define` does, and nothing else does). The entry reads it through bracket access, so `define` — which rewrites the dotted form — leaves that read alone. The question is *"was the value inlined?"*, asked of the global itself; it was once asked as *"is `NODE_ENV` production?"*, which in this project means the opposite of what it looks like (see above) — the deployed app then skipped the only step that could give it a wallet, `ensureIdentity()` threw for want of one, and `/festipod-config.json` sat there served and unasked (`app-security` → [[caveat_shared-wallet-global-before-gate-import]]).
|
||||
|
||||
Practical consequence: to exercise the "shared wallet" flow in dev **end to end** (download plus a working import), pass the REAL password of the e2e wallet **and** the file — the password shown on screen must match the imported `.ngw`, otherwise the import fails (a dummy value such as `1` merely makes the screen appear):
|
||||
|
||||
```
|
||||
FESTIPOD_SHARED_WALLET_PASSWORD=festipod-e2e-tests \
|
||||
|
||||
@@ -14,7 +14,7 @@ A `Dockerfile` exists (multi-stage Bun Alpine). **Installation goes through pnpm
|
||||
|
||||
**`bun` peer pitfall**: `bun-plugin-tailwind` declares `bun` as a peerDependency → pnpm materializes the npm `bun` package and **creates a `node_modules/.bin/bun` shim** that shadows the `bun` from the PATH under `bun run`/`pnpm run`. Its postinstall is ignored by default → broken shim → `bun run start` fails. Fixed by approving the build: `pnpm.onlyBuiltDependencies: ["bun"]` in `package.json` (the postinstall then downloads the real binary). Without that, the whole pnpm migration breaks startup.
|
||||
|
||||
**Quirk**: `start` = `NODE_ENV=production bun src/index.ts` → the container **runs the TypeScript source directly** (Bun transpiles on the fly), it **does not use `dist/`**. `bun run build` (→ `dist/`) is therefore **not** on the default production path. Serving the build would require changing the entrypoint.
|
||||
**Production runs the sources, and this is the normal path, not a quirk**: `start` = `NODE_ENV=production bun src/index.ts` → the container **runs the TypeScript directly** (Bun transpiles on the fly). `bun run build` (→ `dist/`) is on **no** path at all — nothing serves that directory, in this container or anywhere else; serving it would mean changing the entrypoint. Consequence for the code: in this deployment `NODE_ENV=production` says *how* the sources run, never *that they were bundled* — [[knowledge_build-pipeline]].
|
||||
|
||||
## CI/CD
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ summary: Stack components (Bun runtime/build/test, install through pnpm, React,
|
||||
|---|---|
|
||||
| `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/`) |
|
||||
| `build` | `bun run build.ts` — Bun bundler + Tailwind → `dist/` ([[knowledge_build-pipeline]]) |
|
||||
| `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) |
|
||||
| `test:data` | same, with `--tags @data` |
|
||||
|
||||
Reference in New Issue
Block a user