diff --git a/package.json b/package.json index d79ed7f..95c008b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "dependencies": { "@ng-org/alien-deepsignals": "0.1.2-alpha.13", "@ng-org/orm": "0.1.2-alpha.21", - "@ng-org/refine-providers": "0.1.2-alpha.1", "@ng-org/shex-orm": "0.1.2-alpha.9", "@ng-org/web": "0.1.2-alpha.14", "@radix-ui/react-label": "^2.1.7", @@ -42,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", @@ -49,15 +53,10 @@ "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": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cb9e01..52b5757 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: '@ng-org/orm': specifier: 0.1.2-alpha.21 version: 0.1.2-alpha.21(react@19.3.0) - '@ng-org/refine-providers': - specifier: 0.1.2-alpha.1 - version: 0.1.2-alpha.1(@ng-org/alien-deepsignals@0.1.2-alpha.13(react@19.3.0))(@ng-org/orm@0.1.2-alpha.21(react@19.3.0))(@ng-org/shex-orm@0.1.2-alpha.9(typescript@6.0.3))(@ng-org/web@0.1.2-alpha.14)(@refinedev/core@5.0.12(@tanstack/react-query@5.103.2(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)) '@ng-org/shex-orm': specifier: 0.1.2-alpha.9 version: 0.1.2-alpha.9(typescript@6.0.3) @@ -520,15 +517,6 @@ packages: vue: optional: true - '@ng-org/refine-providers@0.1.2-alpha.1': - resolution: {integrity: sha512-MSaVTyCWiqbBR7jn4ESgCk8zYrxWW1EoZCquYAxIud1XBMtkZvl0X/rjIdCPG2GxAVHURC/ut3moavd5Eb+gsA==} - peerDependencies: - '@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 - '@refinedev/core': ^5.0.12 - '@ng-org/shex-orm@0.1.2-alpha.9': resolution: {integrity: sha512-M69oIHrcjl24xKXIaiZjZ64rJvispAPIqn73asLPR73GWpEMvbiot/EN0vDkcfrbRy3cMEql36A9e1Nwv2csOA==} hasBin: true @@ -3615,14 +3603,6 @@ snapshots: optionalDependencies: react: 19.3.0 - '@ng-org/refine-providers@0.1.2-alpha.1(@ng-org/alien-deepsignals@0.1.2-alpha.13(react@19.3.0))(@ng-org/orm@0.1.2-alpha.21(react@19.3.0))(@ng-org/shex-orm@0.1.2-alpha.9(typescript@6.0.3))(@ng-org/web@0.1.2-alpha.14)(@refinedev/core@5.0.12(@tanstack/react-query@5.103.2(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))': - dependencies: - '@ng-org/alien-deepsignals': 0.1.2-alpha.13(react@19.3.0) - '@ng-org/orm': 0.1.2-alpha.21(react@19.3.0) - '@ng-org/shex-orm': 0.1.2-alpha.9(typescript@6.0.3) - '@ng-org/web': 0.1.2-alpha.14 - '@refinedev/core': 5.0.12(@tanstack/react-query@5.103.2(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - '@ng-org/shex-orm@0.1.2-alpha.9(typescript@6.0.3)': dependencies: '@ldo/traverser-shexj': 1.0.0-alpha.28 diff --git a/src/shared/context/FestipodDataContext.tsx b/src/shared/context/FestipodDataContext.tsx index 8fc0606..ce53afd 100644 --- a/src/shared/context/FestipodDataContext.tsx +++ b/src/shared/context/FestipodDataContext.tsx @@ -20,7 +20,6 @@ import { useAccount, normalizeUsername } from './AccountContext'; import { applyIsolation } from '../utils/isolation'; import { ensureAccount, resolveReadGraphs, resolveWriteGraph } from '../utils/storeRegistry'; import { useShapeWithDefaults, type ShapeScope } from '../hooks/useShapeWithDefaults'; -import { RESOURCE_EVENTS } from '../refine/shapeTypes'; import { useBackend } from '../refine/RefineProvider'; // Multi-document mode (storeRegistry): one document per (account × scope), @@ -252,7 +251,8 @@ function useNgData(): FestipodDataContextValue { const { username } = useAccount(); // The selected backend's binding for events — read here, so a switch re-renders // with the new one instead of keeping whichever was current at import time. - const bindings = useBackend().bindings; + // Non-null: `FestipodDataProvider` only mounts this when a backend exists. + const bindings = useBackend()!.bindings; const eventBinding = bindings.event; const profileBinding = bindings.profile; const participationBinding = bindings.participation; @@ -301,7 +301,7 @@ function useNgData(): FestipodDataContextValue { // verifier holds. Pagination is off: the screens show the whole list and the // provider does not paginate at the source anyway. const { result: eventsResult, query: eventsQuery } = useList({ - resource: RESOURCE_EVENTS, + resource: eventBinding.resource, pagination: { mode: 'off' }, }); // The rows carry BOTH ids: Refine's `id` (the DOCUMENT NURI, which is what its @@ -375,7 +375,7 @@ function useNgData(): FestipodDataContextValue { const createEventDoc = useCallback( async (event: Omit): Promise => { const created = await createEventRow({ - resource: RESOURCE_EVENTS, + resource: eventBinding.resource, values: eventBinding.toValues(event), }); return created?.data; @@ -406,7 +406,7 @@ function useNgData(): FestipodDataContextValue { ? eventBinding.toValues({ ...eventBinding.toDomain(row), ...updates }) : eventBinding.toPatch(updates); await updateEventRow({ - resource: RESOURCE_EVENTS, + resource: eventBinding.resource, id: eventBinding.recordKey(row), values, }); @@ -637,8 +637,18 @@ function NgDataProvider({ children }: { children: ReactNode }) { export function FestipodDataProvider({ children }: { children: ReactNode }) { const { status } = useNextGraph(); - console.log('[FestipodData] Provider — NG status:', status); + const backend = useBackend(); + console.log('[FestipodData] Provider — NG status:', status, '| backend:', backend?.name ?? '(none)'); + // CONNECTED BUT NO BACKEND REGISTERED → demo mode, and said out loud. The + // connected path reads and writes through a Refine backend; with none there is + // nothing to read a wallet through, and mounting it anyway would hand every + // screen empty lists that look exactly like an empty wallet. See + // refine/providers/index.ts for why the registry is empty today. + if (status === 'connected' && !backend) { + console.warn('[FestipodData] Connected, but no Refine backend is registered — falling back to demo data'); + return {children}; + } if (status === 'connected') { return {children}; } diff --git a/src/shared/refine/RefineProvider.tsx b/src/shared/refine/RefineProvider.tsx index 0dc8270..0d6d77f 100644 --- a/src/shared/refine/RefineProvider.tsx +++ b/src/shared/refine/RefineProvider.tsx @@ -27,7 +27,8 @@ import { Refine } from '@refinedev/core'; import { getBackend, initialBackendName, resourceNames, type RefineBackend } from './providers'; interface BackendSelection { - backend: RefineBackend; + /** `undefined` when no backend is registered — see providers/index.ts. */ + backend: RefineBackend | undefined; /** Switch backends. The data of the one being left becomes unreachable. */ switchTo(name: string): void; } @@ -38,7 +39,13 @@ const BackendContext = createContext(null); * The current backend. Read it rather than importing one: a module-level import * of a specific backend is what makes it unswitchable. */ -export function useBackend(): RefineBackend { +/** + * The current backend, or `undefined` when none is registered. Callers that READ + * or WRITE data must handle the undefined case — there is nothing to read a + * connected wallet through, and pretending otherwise returns empty lists that + * look like an empty wallet. + */ +export function useBackend(): RefineBackend | undefined { const selection = useContext(BackendContext); if (!selection) { throw new Error('useBackend must be used within a RefineProvider'); @@ -57,11 +64,15 @@ export function useBackendSelection(): BackendSelection { export function RefineProvider({ children }: { children: ReactNode }) { const [name, setName] = useState(initialBackendName); - const backend = useMemo(() => getBackend(name), [name]); + const backend = useMemo(() => (name ? getBackend(name) : undefined), [name]); // Built once PER BACKEND, not per render: a fresh provider identity makes Refine // re-run every query behind it. const providers = useMemo(() => { + if (!backend) { + console.log('[refine] no backend registered — the app runs in demo mode'); + return undefined; + } console.log(`[refine] backend: ${backend.name}`); return backend.build(); }, [backend]); @@ -74,6 +85,13 @@ export function RefineProvider({ children }: { children: ReactNode }) { const selection = useMemo(() => ({ backend, switchTo }), [backend, switchTo]); + // NO BACKEND, NO . Mounting it without a data provider would give the + // hooks something that answers nothing, which reads downstream as an empty + // wallet. The selection context is still published, so a selector can appear. + if (!backend || !providers) { + return {children}; + } + return ( { - await ngLogin(); - return { success: true }; - }, - - logout: async () => { - await logoutNg(); - return { success: true }; - }, - - /** - * Authenticated = the NextGraph session is open. `AuthGate` is what actually - * gates the screens (it reads `NextGraphContext`), so no `redirectTo` is - * returned here: Refine has no router in this app and must not try to route. - */ - check: async () => ({ authenticated: session !== undefined }), - - onError: async (error) => ({ error }), - - /** - * The NextGraph session carries the wallet user id under `user` when one is - * open. Festipod's PERCEIVED login (the chosen username) lives in - * `AccountContext`, which is React state and not reachable from here — so - * this reports the technical identity only, and `undefined` when there is none. - */ - getIdentity: async () => { - const user = session?.user; - return typeof user === 'string' && user ? { id: user } : undefined; - }, - - /** - * The handle the data provider actually consumes. Festipod's - * `NextGraphSession` is the same object `@ng-org/web` hands its `init` - * callback; the cast only reconciles the two local declarations of it. - */ - getSession: () => session as Session | undefined, - }; -} diff --git a/src/shared/refine/providers/index.ts b/src/shared/refine/providers/index.ts index 09449c5..1ceb7ae 100644 --- a/src/shared/refine/providers/index.ts +++ b/src/shared/refine/providers/index.ts @@ -1,53 +1,53 @@ /** * The backend registry — which Refine backends exist, and which one starts. * - * SWITCHING IS MEANT TO HAPPEN AT RUNTIME, so nothing here is resolved at module - * scope: a module-level `const backend = …` is precisely what makes a backend - * unswitchable. The current one lives in React state, held by `RefineProvider`, - * and consumers read it through `useBackend()`. The build-time global below only - * decides which one is selected FIRST. + * IT IS EMPTY ON PURPOSE, and that is the current state of the work rather than + * an oversight. Festipod must end up supporting SEVERAL Refine backends, and the + * seam for that is what this directory is: the contract in `types.ts`, the + * selection below, `RefineProvider`'s runtime switch. What is missing is backends. * - * Leaving a backend loses access to its data, and that is accepted: each holds its - * own behind its own sign-in, and nothing is migrated between them. + * WHY THE FIRST ONE WAS WITHDRAWN. `@ng-org/refine-providers` was wired and + * measured, and it works as plain CRUD over SHEX shapes. But Festipod's sign-up + * model needs more than CRUD: `participantCount` counts participants this session + * cannot see, which the project derives from inbox deposits recomputed by the + * event's owner — see the requirement in `types.ts`. A counter incremented from a + * value read back out of a list is a different mechanism, it races itself, and the + * project's own brief declares it superseded. Rather than harden a counter on a + * backend that structurally cannot carry the model, the wiring was removed; the + * worked implementation is in this branch's history (`providers/nextgraph.ts`, + * up to the commit that withdrew it) and is the reference for the shape of the + * next one. * - * There is no selector screen yet — that is interface work, not seam work. + * WHAT COMES NEXT, and why the seam stays: a Refine provider over NextGraph *plus + * the polyfill*, which does publish inboxes, and `@activitypods/refine-providers`. + * Festipod has to support both, so the structure is kept and only the entry is + * gone. * - * ADDING A BACKEND is one file implementing `RefineBackend`, plus one line here. - * What that file must answer is not negotiable and cannot be guessed — see - * `types.ts`: how its providers are configured, what its records look like, which - * key addresses a record versus which id the app references, and whether its - * `update` replaces or patches. + * ADDING ONE is a file implementing `RefineBackend` plus a line here. What that + * file must answer is in `types.ts` and cannot be guessed: how its providers are + * configured, what its records look like, which key addresses a record versus + * which id the app references, whether its `update` replaces or patches, and + * whether it can derive the participant count. * - * `@activitypods/refine-providers` is the next candidate, and two of those four - * are already known from its published configuration: providers take - * `resources: { events: { shapeTreeUri } }` plus a `jsonContext`, and auth is - * Solid-OIDC with a `clientId`. The other two — its record shape and whether - * `update` patches — are not, and neither is the question that gates the whole - * thing: it declares resources in ActivityStreams, while Festipod persists `fp:` - * shapes. `as:Event` exists; a meeting point, the product's unit of value, has no - * obvious ActivityStreams equivalent. That has to be settled before an adapter - * can be written rather than invented. + * WITH NO BACKEND the app runs in demo mode: `FestipodDataProvider` has nothing + * to read a connected wallet through, and says so rather than mounting a data + * path that would return nothing. */ -import { nextgraphBackend } from './nextgraph'; import type { RefineBackend } from './types'; export type { RecordBinding, RecordBindings, RefineBackend } from './types'; export { resourceNames } from './types'; -const BACKENDS: Record = { - [nextgraphBackend.name]: nextgraphBackend, -}; +const BACKENDS: Record = {}; // Build-injected, same plumbing as the other build-time globals (build.ts -// `define`s it). Absent → the default below, so nothing has to be configured. +// `define`s it). Absent → the first registered backend, if there is one. declare global { // eslint-disable-next-line no-var var __FESTIPOD_BACKEND__: string | undefined; } -const DEFAULT_BACKEND = nextgraphBackend.name; - /** Every registered backend — what a selector would offer. */ export function listBackends(): RefineBackend[] { return Object.values(BACKENDS); @@ -61,14 +61,22 @@ export function listBackends(): RefineBackend[] { export function getBackend(name: string): RefineBackend { const backend = BACKENDS[name]; if (!backend) { - throw new Error( - `Unknown Refine backend "${name}" — known: ${Object.keys(BACKENDS).join(', ')}`, - ); + const known = Object.keys(BACKENDS).join(', ') || '(none registered)'; + throw new Error(`Unknown Refine backend "${name}" — known: ${known}`); } return backend; } -/** Which backend is selected when the app starts. */ -export function initialBackendName(): string { - return globalThis.__FESTIPOD_BACKEND__ || DEFAULT_BACKEND; +/** + * Which backend is selected when the app starts, or `undefined` when none is + * registered. A requested name that does not exist still throws — asking for a + * specific backend and silently getting another is the failure worth avoiding. + */ +export function initialBackendName(): string | undefined { + const requested = globalThis.__FESTIPOD_BACKEND__; + if (requested) { + getBackend(requested); + return requested; + } + return Object.keys(BACKENDS)[0]; } diff --git a/src/shared/refine/providers/nextgraph.ts b/src/shared/refine/providers/nextgraph.ts deleted file mode 100644 index bbf721b..0000000 --- a/src/shared/refine/providers/nextgraph.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * The NextGraph backend — `@ng-org/refine-providers` over the wallet this - * deployment already opens. - * - * Everything backend-specific that used to sit inline in `FestipodDataContext` - * lives here: the `fp:` vocabulary, the two identifiers, and the fact that this - * provider's `update` replaces a record rather than patching it. - */ - -import { dataProvider, liveProvider } from '@ng-org/refine-providers'; -import type { BaseKey } from '@refinedev/core'; -import type { FpEventData, FpParticipationData, FpUserData } from '../../data/types'; -import type { FpEvent, FpParticipation, FpUserProfile } from '../../shapes/orm/festipodShapes.typings'; -import shapeTypes, { - RESOURCE_EVENTS, - RESOURCE_PARTICIPATIONS, - RESOURCE_PROFILES, -} from '../shapeTypes'; -import { createNgAuthProvider } from '../ngAuthProvider'; -import type { RecordBinding, RefineBackend } from './types'; - -/** - * The RDF classes Festipod persists. These IRIs ARE the stored data — renaming - * one renames what is on disk, so they are never adjusted for convenience. - */ -const TYPE = { - event: 'http://festipod.org/Event', - profile: 'http://festipod.org/UserProfile', - participation: 'http://festipod.org/Participation', -} as const; - -/** - * Shared by every binding here: this provider addresses a record by the DOCUMENT - * NURI, while the app's entities reference each other by the SUBJECT IRI. One - * document per record keeps them 1-1, but they are different strings and `update` - * only accepts the first. - */ -const recordKey = (record: unknown): BaseKey => - (record as { '@graph': string })['@graph'] as BaseKey; -const domainId = (record: unknown): string => (record as { '@id': string })['@id']; - -/** This provider's `update` deletes the old object and adds one built from values. */ -const replaces = { - updateReplaces: true as const, - toPatch: (): never => { - throw new Error('nextgraph: update replaces the record — send the whole of it, not a patch'); - }, -}; - -const eventBinding: RecordBinding = { - resource: RESOURCE_EVENTS, - // Events are what other users must find — see concept `functional-domain`. - visibility: 'public', - recordKey, - domainId, - ...replaces, - - toDomain: (record) => { - const e = record as FpEvent; - return { - id: e['@id'], - title: e.title, - description: e.description || '', - date: e.date, - location: e.location, - distance: e.distance, - participantCount: e.participantCount, - coverImage: e.coverImage, - hostName: e.hostName, - hostInitials: e.hostInitials, - }; - }, - - // `@type` IS NOT OPTIONAL. The data provider writes a hardcoded ActivityStreams - // `Note` type — the type of the example app it was written against — and spreads - // the caller's values AFTER it, so passing `@type` is what makes the record a - // Festipod Event. Drop it and the shape stops matching on read. - toValues: (data) => ({ - '@type': TYPE.event, - title: data.title, - description: data.description, - date: data.date, - location: data.location, - distance: data.distance, - participantCount: data.participantCount, - coverImage: data.coverImage, - hostName: data.hostName, - hostInitials: data.hostInitials, - }), -}; - -const profileBinding: RecordBinding = { - resource: RESOURCE_PROFILES, - // A profile is readable by one's network, not by everyone. - visibility: 'protected', - recordKey, - domainId, - ...replaces, - - toDomain: (record) => { - const u = record as FpUserProfile; - return { - id: u['@id'], - name: u.name, - initials: u.initials, - username: u.username, - role: u.role, - isPublic: u.isPublic, - }; - }, - - toValues: (data) => ({ - '@type': TYPE.profile, - name: data.name, - initials: data.initials, - username: data.username, - role: data.role, - isPublic: data.isPublic, - }), -}; - -const participationBinding: RecordBinding = { - resource: RESOURCE_PARTICIPATIONS, - visibility: 'protected', - recordKey, - domainId, - ...replaces, - - // `fp:event` and `fp:user` hold SUBJECT IRIs, which is why `domainId` above is - // the subject and not the document: these references resolve against it. - toDomain: (record) => { - const p = record as FpParticipation; - return { - id: p['@id'], - eventId: p.event, - userId: p.user, - isConfirmed: p.isConfirmed, - }; - }, - - toValues: (data) => ({ - '@type': TYPE.participation, - event: data.eventId, - user: data.userId, - isConfirmed: data.isConfirmed, - }), -}; - -export const nextgraphBackend: RefineBackend = { - name: 'nextgraph', - bindings: { - event: eventBinding, - profile: profileBinding, - participation: participationBinding, - // meetingPoint: pending its SHEX shape — see types.ts. - }, - build: () => { - // Festipod's own AuthProvider, not the package's — see ngAuthProvider for why - // a second `init()` on one WASM session is a collision, not a convenience. - const authProvider = createNgAuthProvider(); - return { - authProvider, - dataProvider: dataProvider({ shapeTypes, authProvider }), - liveProvider: liveProvider({ shapeTypes }), - }; - }, -}; diff --git a/src/shared/refine/providers/types.ts b/src/shared/refine/providers/types.ts index 4f8ec66..7af642b 100644 --- a/src/shared/refine/providers/types.ts +++ b/src/shared/refine/providers/types.ts @@ -111,6 +111,31 @@ export interface RefineBackend { readonly bindings: RecordBindings; } +/** + * WHAT A BACKEND MUST BE ABLE TO DO, beyond CRUD — recorded here because the + * first backend tried could NOT, and that is why it was withdrawn. + * + * `participantCount` is **the number of participants, including those this + * session cannot see**. So it is not a count of the participations we can read: + * filtering by what we are allowed to see would under-report it. The project's + * model (concept `data-layer`, brief_2026-07-20_attendance-set-model, and + * knowledge_context-internals) derives it instead: + * + * - whoever signs up writes their OWN participation, then DEPOSITS a marker + * into the event's inbox; + * - the event OWNER's session watches the inboxes of its own events and + * RECOMPUTES the count from the deposits, deduped, minus the cancelled ones. + * It is the field's only writer, and the count is `|active set|`. + * + * Two consequences for this contract. A backend offering only CRUD over records + * — no inbox, no per-document delivery — cannot support it: incrementing a + * counter read back from a list is a different mechanism, it races itself, and + * the project's brief declares it superseded. And a backend that CAN must say so, + * because the app's sign-up path differs accordingly. + * + * Until a backend provides it, `participantCount` is whatever the record carries. + */ + /** Every resource name a backend serves, derived from its bindings. */ export function resourceNames(backend: RefineBackend): string[] { return [...new Set(Object.values(backend.bindings).map((b) => b.resource))]; diff --git a/src/shared/refine/shapeTypes.ts b/src/shared/refine/shapeTypes.ts deleted file mode 100644 index 5456e8f..0000000 --- a/src/shared/refine/shapeTypes.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * The Refine RESOURCE MAP — the one place that says which Refine resource name - * stands for which SHEX shape. - * - * `@ng-org/refine-providers` identifies data by shape type, not by endpoint: its - * data provider looks `shapeTypes[resource]` up on every call, so a resource name - * that is absent here silently resolves to `undefined` and the read comes back - * empty — no error, just nothing. Every `useList` / `useOne` / `useCreate` / - * `useUpdate` call site must therefore name a resource declared below. - * - * These names are the NextGraph backend's, which is why this file lives beside it - * rather than in the app: another backend keys its own configuration by its own - * names, and nothing says the two must agree on a spelling. - */ - -import type { ShapeTypes } from '@ng-org/refine-providers'; -import { - FpEventShapeType, - FpParticipationShapeType, - FpUserProfileShapeType, -} from '../shapes/orm/festipodShapes.shapeTypes'; - -/** Resource names. Import them — never retype the strings. */ -export const RESOURCE_EVENTS = 'events'; -export const RESOURCE_PROFILES = 'profiles'; -export const RESOURCE_PARTICIPATIONS = 'participations'; - -const shapeTypes: ShapeTypes = { - [RESOURCE_EVENTS]: FpEventShapeType, - [RESOURCE_PROFILES]: FpUserProfileShapeType, - [RESOURCE_PARTICIPATIONS]: FpParticipationShapeType, -}; - -export default shapeTypes; diff --git a/src/shared/test-harness/harness-ng.tsx b/src/shared/test-harness/harness-ng.tsx index 22773cc..c66ff83 100644 --- a/src/shared/test-harness/harness-ng.tsx +++ b/src/shared/test-harness/harness-ng.tsx @@ -10,7 +10,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { NextGraphProvider, useNextGraph } from '../context/NextGraphContext'; -import { RefineProvider } from '../refine/RefineProvider'; +import { RefineProvider, useBackend } from '../refine/RefineProvider'; import { FestipodDataProvider, useFestipodData } from '../context/FestipodDataContext'; import type { FpEventData, FpParticipationData, FpUserData } from '../data/types'; import { useShapeSet, useShapeSubscription } from '../hooks/useShapeSet'; @@ -46,6 +46,18 @@ function DataHarnessNG() { // Wait for NG connection before exposing the test bridge function HarnessRouter() { const { status } = useNextGraph(); + const backend = useBackend(); + + // NO BACKEND, NO BRIDGE — and this refusal is the point. Without one the data + // context falls back to demo fixtures, so a @data scenario would read seeded + // events, assert happily on them and report GREEN while touching no wallet at + // all. A layer that exists to test broker persistence must fail rather than + // pass on fixtures. `reason` is published so the step's timeout is diagnosable + // instead of mute. + if (!backend) { + (window as any).__testData = { ready: false, reason: 'no-refine-backend' }; + return
NO_REFINE_BACKEND
; + } if (status === 'connected') { return ;