feat(data): logs [app][data] identité-first + participantCount avant→après
Préfixe identité-first ; label participation ; valeur compteur avant/après écriture owner + lecture affichage. Additif. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
@@ -168,41 +168,44 @@ function useLocalData(empty?: boolean): FestipodDataContextValue {
|
||||
? users.find(u => normalizeIdentifier(u.username) === normalizeIdentifier(identifier))
|
||||
: undefined;
|
||||
const currentUserId = empty ? '' : (accountUser?.id ?? CURRENT_USER_ID);
|
||||
// Identity-first log prefix: current user id when resolved, else the bare
|
||||
// `[app][data]` form (e.g. the transient `empty` connecting state).
|
||||
const logPrefix = currentUserId ? `[${currentUserId}][app][data]` : '[app][data]';
|
||||
const currentUser = users.find(u => u.id === currentUserId);
|
||||
const selectedEvent = events.find(e => e.id === selectedEventId);
|
||||
const selectedUser = users.find(u => u.id === selectedUserId);
|
||||
|
||||
const queries = buildQueries(events, users, participations, meetingPoints, friendships, currentUserId);
|
||||
|
||||
console.log('[FestipodData] Render —', empty ? 'connecting (empty)' : 'local',
|
||||
console.log(`${logPrefix} Render —`, empty ? 'connecting (empty)' : 'local',
|
||||
'| events:', events.length,
|
||||
'| selectedEvent:', selectedEvent?.title ?? '(none)');
|
||||
|
||||
// Local mode: mutations are no-ops (static defaults)
|
||||
const createEvent = useCallback(async (event: Omit<FpEventData, 'id'>): Promise<FpEventData> => {
|
||||
console.log('[FestipodData] createEvent (local, no-op):', event.title);
|
||||
console.log(`${logPrefix} createEvent (local, no-op):`, event.title);
|
||||
return { ...event, id: nextId('event') };
|
||||
}, []);
|
||||
}, [logPrefix]);
|
||||
const updateEvent = useCallback((_id: string, _updates: Partial<FpEventData>) => {
|
||||
console.log('[FestipodData] updateEvent (local, no-op)');
|
||||
}, []);
|
||||
console.log(`${logPrefix} updateEvent (local, no-op)`);
|
||||
}, [logPrefix]);
|
||||
const joinEvent = useCallback((_eventId: string) => {
|
||||
console.log('[FestipodData] joinEvent (local, no-op)');
|
||||
}, []);
|
||||
console.log(`${logPrefix} joinEvent (local, no-op)`);
|
||||
}, [logPrefix]);
|
||||
const leaveEvent = useCallback((_eventId: string) => {
|
||||
console.log('[FestipodData] leaveEvent (local, no-op)');
|
||||
}, []);
|
||||
console.log(`${logPrefix} leaveEvent (local, no-op)`);
|
||||
}, [logPrefix]);
|
||||
const addMeetingPoint = useCallback((_mp: Omit<FpMeetingPointData, 'id'>) => {
|
||||
console.log('[FestipodData] addMeetingPoint (local, no-op)');
|
||||
}, []);
|
||||
console.log(`${logPrefix} addMeetingPoint (local, no-op)`);
|
||||
}, [logPrefix]);
|
||||
const addFriend = useCallback((_friendId: string) => {
|
||||
console.log('[FestipodData] addFriend (local, no-op)');
|
||||
}, []);
|
||||
console.log(`${logPrefix} addFriend (local, no-op)`);
|
||||
}, [logPrefix]);
|
||||
const updateProfile = useCallback((_updates: Partial<FpUserData>) => {
|
||||
console.log('[FestipodData] updateProfile (local, no-op)');
|
||||
}, []);
|
||||
console.log(`${logPrefix} updateProfile (local, no-op)`);
|
||||
}, [logPrefix]);
|
||||
const loadTestData = useCallback(async (): Promise<BootstrapResult> => {
|
||||
console.log('[FestipodData] loadTestData (local, no-op)');
|
||||
console.log(`${logPrefix} loadTestData (local, no-op)`);
|
||||
return { seeded: false, userIdMap: new Map(), eventIdMap: new Map(), createdDocs: { public: [], protected: [] } };
|
||||
}, []);
|
||||
|
||||
@@ -386,7 +389,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
if (cancelled) return;
|
||||
setOwnedEventIds(prev => [...new Set([...prev, ...myPublic])]);
|
||||
} catch (err) {
|
||||
console.error('[FestipodData] owned-events resolution failed:', err);
|
||||
console.error(`${logPrefix} owned-events resolution failed:`, err);
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
@@ -429,14 +432,14 @@ function useNgData(): FestipodDataContextValue {
|
||||
if (!readReady) return; // still syncing — do NOT mistake pending for empty
|
||||
const walletHasData = events.length > 0 || users.length > 0;
|
||||
if (!shouldAutoSeed(walletHasData)) {
|
||||
console.log('[FestipodData] Auto-seed (FESTIPOD_AUTO_SEED): wallet already has data — skip');
|
||||
console.log(`${logPrefix} Auto-seed (FESTIPOD_AUTO_SEED): wallet already has data — skip`);
|
||||
return;
|
||||
}
|
||||
// Enabled AND synced-empty → a real empty wallet. Seed once.
|
||||
hasTriedAutoSeed.current = true;
|
||||
console.log('[FestipodData] Auto-seed (FESTIPOD_AUTO_SEED): wallet empty (synced), bootstrapping…');
|
||||
console.log(`${logPrefix} Auto-seed (FESTIPOD_AUTO_SEED): wallet empty (synced), bootstrapping…`);
|
||||
bootstrapWallet(false, createEntityDoc, identifier || undefined)
|
||||
.catch(err => console.error('[FestipodData] Auto-seed failed:', err));
|
||||
.catch(err => console.error(`${logPrefix} Auto-seed failed:`, err));
|
||||
// The reactive `watchShape` reads pick the seeded per-entity docs up on their
|
||||
// own (each createEntityDoc appends to the scope index → the container-index
|
||||
// subscription re-resolves → the new docs enter the read). No registerDoc/relist.
|
||||
@@ -460,7 +463,21 @@ function useNgData(): FestipodDataContextValue {
|
||||
// back to the read profile's IRI only when there is no login (dev/demo).
|
||||
const currentUserId =
|
||||
(identifier ? `urn:festipod:user:${normalizeIdentifier(identifier)}` : (currentUser?.id || ''));
|
||||
// Identity-first log prefix, reused by every DATA log below (including the
|
||||
// closures defined earlier in this function body — they only execute after
|
||||
// this render has finished, by which point `logPrefix` is initialized).
|
||||
const logPrefix = currentUserId ? `[${currentUserId}][app][data]` : '[app][data]';
|
||||
const selectedEvent = events.find(e => e.id === selectedEventId);
|
||||
// DISPLAY READ — log participantCount exactly as currently exposed for
|
||||
// rendering. Compared against the owner-materializer's WRITE logs below, this
|
||||
// pinpoints whether a stuck counter is a DATA problem (never incremented) or a
|
||||
// DISPLAY/read problem (incremented but not re-read until the next session).
|
||||
if (selectedEvent) {
|
||||
console.log(
|
||||
`${logPrefix} participation read for display — event=${canonicalEventId(selectedEvent.id)} ` +
|
||||
`"${selectedEvent.title}": participantCount lu = ${selectedEvent.participantCount}`,
|
||||
);
|
||||
}
|
||||
const selectedUser = users.find(u => u.id === selectedUserId);
|
||||
|
||||
// --- OWNER MATERIALIZER (Option B, brief §B.2 + T02.c notifications) -------
|
||||
@@ -518,11 +535,23 @@ function useNgData(): FestipodDataContextValue {
|
||||
// deposits inside `materializeAttendance` / `readRegistrationNotifications`.
|
||||
const targetInbox = await hostInboxNuri('');
|
||||
console.log(
|
||||
`[Attendance] owner materialize START (trigger=${trigger}) — ${owned.length} owned ` +
|
||||
`${logPrefix} owner participation materialize START (trigger=${trigger}) — ${owned.length} owned ` +
|
||||
`event(s), inbox=${targetInbox}`,
|
||||
);
|
||||
const notifs: FpNotificationData[] = [];
|
||||
for (const evId of owned) {
|
||||
// BEFORE — the event's readable detail (short id + title) and the
|
||||
// participantCount value as currently READ/exposed (the app-side `events`
|
||||
// state), captured before this cycle's derive+write. Comparing this to the
|
||||
// AFTER log below tells whether the counter is a DATA problem (never
|
||||
// incremented) or a DISPLAY/read problem (incremented but not re-read).
|
||||
const knownEvent = events.find(e => e.id === evId);
|
||||
const knownCount = knownEvent?.participantCount;
|
||||
console.log(
|
||||
`${logPrefix} participation materialize — event=${canonicalEventId(evId)}` +
|
||||
(knownEvent?.title ? ` "${knownEvent.title}"` : '') +
|
||||
` — participantCount before write (as currently read) = ${knownCount ?? '(unknown)'}`,
|
||||
);
|
||||
// (1) COUNT — derive the distinct active-registration set for this event
|
||||
// and write it on MY OWN event doc (only when it changed). The read inside
|
||||
// `materializeAttendance` is BARRIER-GATED (`inbox.readSynced`): at the
|
||||
@@ -540,21 +569,34 @@ function useNgData(): FestipodDataContextValue {
|
||||
if (prevCount !== nextCount) {
|
||||
materializedCountRef.current.set(evId, nextCount);
|
||||
console.log(
|
||||
`[Attendance] owner materialize — event=${canonicalEventId(evId)}: ` +
|
||||
`${logPrefix} owner participation materialize — event=${canonicalEventId(evId)}: ` +
|
||||
`participantCount ${prevCount ?? '(none)'} → ${nextCount} (writing own doc)`,
|
||||
);
|
||||
// The write lands on the owned event doc, which `watchShape('public')`
|
||||
// already subscribes → the reactive read re-renders the new count on
|
||||
// the broker push (no manual re-query).
|
||||
let writeOk = true;
|
||||
await updateEntityField(evId, evId, 'participantCount', int(nextCount))
|
||||
.catch(err => {
|
||||
// Revert the memo so a transient write failure retries next trigger.
|
||||
writeOk = false;
|
||||
materializedCountRef.current.delete(evId);
|
||||
console.error('[Attendance] owner materialize count WRITE FAILED:', err);
|
||||
console.error(`${logPrefix} owner participation materialize count WRITE FAILED:`, err);
|
||||
});
|
||||
if (writeOk) {
|
||||
// AFTER — the write has landed on the owner's own doc. N → M reuses the
|
||||
// SAME "before" reference point logged above, so it is directly
|
||||
// comparable: a DISPLAY-read log (see `selectedEvent`, above in this
|
||||
// file) still showing the old N after this fires means the counter data
|
||||
// is fine and it is the read side that lags.
|
||||
console.log(
|
||||
`${logPrefix} participation materialize — event=${canonicalEventId(evId)}: ` +
|
||||
`participantCount AFTER write = ${knownCount ?? '(unknown)'} → ${nextCount}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`[Attendance] owner materialize — event=${canonicalEventId(evId)}: ` +
|
||||
`${logPrefix} owner participation materialize — event=${canonicalEventId(evId)}: ` +
|
||||
`participantCount unchanged (${nextCount}) — no write`,
|
||||
);
|
||||
}
|
||||
@@ -571,7 +613,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Attendance] owner materialization failed:', err);
|
||||
console.error(`${logPrefix} owner participation materialization failed:`, err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -634,7 +676,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
events, users, participations, meetingPoints, friendships, currentUserId,
|
||||
);
|
||||
|
||||
console.log('[FestipodData] Render — NG | events:', events.length,
|
||||
console.log(`${logPrefix} Render — NG | events:`, events.length,
|
||||
'| users:', users.length, '| participations:', participations.length,
|
||||
'| selectedEvent:', selectedEvent?.title ?? '(none)');
|
||||
|
||||
@@ -647,7 +689,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
// private) — the app carries no access logic.
|
||||
|
||||
const createEvent = useCallback(async (event: Omit<FpEventData, 'id'>): Promise<FpEventData> => {
|
||||
console.log('[FestipodData] createEvent (NG):', event.title);
|
||||
console.log(`${logPrefix} createEvent (NG):`, event.title);
|
||||
// Owner principal = the account identifier (what setCurrentUser declares). The
|
||||
// SDK create returns THIS entity's OWN public document and declares its
|
||||
// ReadCap policy (public → world-readable). Fall back to a generic account
|
||||
@@ -697,13 +739,13 @@ function useNgData(): FestipodDataContextValue {
|
||||
submitEventToIndex(
|
||||
{ doc: eventGraph, id: addedEvent["@id"], title: event.title },
|
||||
null,
|
||||
).catch(err => console.error('[FestipodData] submit event to index failed:', err));
|
||||
).catch(err => console.error(`${logPrefix} submit event to index failed:`, err));
|
||||
}
|
||||
return { ...event, id: addedEvent?.["@id"] || `ng-pending-${Date.now()}` };
|
||||
}, [currentUserId, identifier]);
|
||||
|
||||
const updateEvent = useCallback(async (id: string, updates: Partial<FpEventData>) => {
|
||||
console.log('[FestipodData] updateEvent (NG):', id, updates);
|
||||
console.log(`${logPrefix} updateEvent (NG):`, id, updates);
|
||||
// The event's `@id` IS its own document NURI (one entity = one document), so
|
||||
// it is both the write graph and the subject. Persist each provided mutable
|
||||
// field DIRECTLY via SPARQL (the durable write); `watchShape` re-reads on the
|
||||
@@ -718,19 +760,19 @@ function useNgData(): FestipodDataContextValue {
|
||||
if (updates.date !== undefined) persists.push(updateEntityField(graph, id, 'date', str(updates.date)));
|
||||
if (updates.location !== undefined) persists.push(updateEntityField(graph, id, 'location', str(updates.location)));
|
||||
if (updates.distance !== undefined) persists.push(updateEntityField(graph, id, 'distance', flt(updates.distance)));
|
||||
await Promise.all(persists).catch(err => console.error('[FestipodData] persist event update failed:', err));
|
||||
await Promise.all(persists).catch(err => console.error(`${logPrefix} persist event update failed:`, err));
|
||||
}, []);
|
||||
|
||||
const joinEvent = useCallback(async (eventId: string, userId?: string) => {
|
||||
const uid = userId || currentUserId;
|
||||
console.log('[FestipodData] joinEvent (NG):', eventId, 'user:', uid);
|
||||
console.log(`${logPrefix} joinEvent (NG):`, eventId, 'user:', uid);
|
||||
// A Participation MUST carry a user principal (SHEX `fp:user` is mandatory) —
|
||||
// writing one without it produces an entity the ORM drops on read (the
|
||||
// participation silently never round-trips). Refuse an empty principal rather
|
||||
// than persist a broken participation. The caller resolves a real user id (the
|
||||
// current user's IRI) before joining.
|
||||
if (!uid) {
|
||||
console.error('[FestipodData] joinEvent: empty user principal — refusing to write a participation with no fp:user.');
|
||||
console.error(`${logPrefix} joinEvent: empty user principal — refusing to write a participation with no fp:user.`);
|
||||
return;
|
||||
}
|
||||
// IDEMPOTENCE — check AUTHORITATIVELY against the broker, not the reactive set.
|
||||
@@ -739,7 +781,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
// one participation"). The broker query sees the real state regardless of lag.
|
||||
const already = await countUserParticipations(identifier || uid || 'anon', eventId, uid).catch(() => 0);
|
||||
if (already > 0) {
|
||||
console.log('[FestipodData] Already participating (broker-confirmed), skipping');
|
||||
console.log(`${logPrefix} Already participating (broker-confirmed), skipping`);
|
||||
return;
|
||||
}
|
||||
// 1) Persist the Participation as its OWN document in the PROTECTED scope
|
||||
@@ -787,7 +829,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
// Carry the joiner's participation-doc NURI so the owner (if a connection)
|
||||
// could read it in clear; the count itself does not depend on reading it.
|
||||
console.log(
|
||||
`[Attendance] joinEvent — depositing registration into event inbox: ` +
|
||||
`${logPrefix} joinEvent — depositing participation registration into event inbox: ` +
|
||||
`event=${canonicalEventId(eventId)} user=${uid} (count now moves via the OWNER ` +
|
||||
`materializing this deposit on its own doc, at its next connection)`,
|
||||
);
|
||||
@@ -805,13 +847,13 @@ function useNgData(): FestipodDataContextValue {
|
||||
// notification id from the inbox and same-ms/anon deposits never collide.
|
||||
setNotifications(prev => [...prev, { ...notif, id: `notif-${depositUid}` }]);
|
||||
} catch (err) {
|
||||
console.error('[FestipodData] joinEvent inbox/notify failed:', err);
|
||||
console.error(`${logPrefix} joinEvent inbox/notify failed:`, err);
|
||||
}
|
||||
}, [events, currentUserId, identifier]);
|
||||
|
||||
const leaveEvent = useCallback(async (eventId: string, userId?: string) => {
|
||||
const uid = userId || currentUserId;
|
||||
console.log('[FestipodData] leaveEvent (NG):', eventId, 'user:', uid);
|
||||
console.log(`${logPrefix} leaveEvent (NG):`, eventId, 'user:', uid);
|
||||
// Find the participation in the union-read set. Each participation is its OWN
|
||||
// document (writeEntity uses the doc NURI as the subject), so `part.id` is BOTH
|
||||
// the subject IRI AND the graph NURI it lives in.
|
||||
@@ -828,14 +870,14 @@ function useNgData(): FestipodDataContextValue {
|
||||
try {
|
||||
result = await deleteParticipation(graphNuri, eventId, uid, subjectIri);
|
||||
} catch (err) {
|
||||
console.error('[FestipodData] SPARQL DELETE participation failed:', err);
|
||||
console.error(`${logPrefix} SPARQL DELETE participation failed:`, err);
|
||||
throw err instanceof Error ? err : new Error(String(err));
|
||||
}
|
||||
// AUTHORITATIVE verification: only proceed once the broker RE-QUERY confirms
|
||||
// the participation is gone (remaining === 0). If the delete matched nothing,
|
||||
// surface it rather than falsely flip the UI (it would resurrect on re-sync).
|
||||
if (result.remaining > 0) {
|
||||
const msg = `[FestipodData] leaveEvent: SPARQL delete removed nothing ` +
|
||||
const msg = `${logPrefix} leaveEvent: SPARQL delete removed nothing ` +
|
||||
`(before=${result.before}, remaining=${result.remaining}, bySubject=${result.bySubject}) ` +
|
||||
`for event=${eventId} user=${uid} — NOT flipping UI (would resurrect).`;
|
||||
console.error(msg);
|
||||
@@ -871,13 +913,13 @@ function useNgData(): FestipodDataContextValue {
|
||||
// otherwise the owner falls back to (eventId, userId) matching.
|
||||
const regUid = joinUidsRef.current.get(`${eventId}|${uid}`);
|
||||
console.log(
|
||||
`[Attendance] leaveEvent — depositing leave marker into event inbox: ` +
|
||||
`${logPrefix} leaveEvent — depositing participation leave marker into event inbox: ` +
|
||||
`event=${canonicalEventId(eventId)} user=${uid} regUid=${regUid ?? '(none)'}`,
|
||||
);
|
||||
await depositLeave(targetInbox, eventId, registrantId, regUid);
|
||||
joinUidsRef.current.delete(`${eventId}|${uid}`);
|
||||
} catch (err) {
|
||||
console.error('[FestipodData] leaveEvent inbox deposit failed:', err);
|
||||
console.error(`${logPrefix} leaveEvent inbox deposit failed:`, err);
|
||||
}
|
||||
// The participation doc is subscribed by `watchShape('protected')`; the SPARQL
|
||||
// DELETE pushes → the reactive read drops it (`isParticipating` reflects it).
|
||||
@@ -900,7 +942,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
}, [currentUserId]);
|
||||
|
||||
const updateProfile = useCallback(async (updates: Partial<FpUserData>) => {
|
||||
console.log('[FestipodData] updateProfile (NG):', updates);
|
||||
console.log(`${logPrefix} updateProfile (NG):`, updates);
|
||||
// The current user's profile is its own document (subject IRI = doc NURI).
|
||||
const target = currentUser ?? users[0];
|
||||
if (!target) return;
|
||||
@@ -911,11 +953,11 @@ function useNgData(): FestipodDataContextValue {
|
||||
if (updates.username !== undefined) persists.push(updateEntityField(graph, graph, 'username', str(updates.username)));
|
||||
if (updates.role !== undefined) persists.push(updateEntityField(graph, graph, 'role', str(updates.role)));
|
||||
if (updates.isPublic !== undefined) persists.push(updateEntityField(graph, graph, 'isPublic', bool(updates.isPublic)));
|
||||
await Promise.all(persists).catch(err => console.error('[FestipodData] persist profile update failed:', err));
|
||||
await Promise.all(persists).catch(err => console.error(`${logPrefix} persist profile update failed:`, err));
|
||||
}, [currentUser, users]);
|
||||
|
||||
const loadTestData = useCallback(async (): Promise<BootstrapResult> => {
|
||||
console.log('[FestipodData] loadTestData (NG)');
|
||||
console.log(`${logPrefix} loadTestData (NG)`);
|
||||
// An EXPLICIT load is authoritative — SUPPRESS the dev auto-seed so only ONE
|
||||
// seed runs (marking the guard at the START, before the awaited seed, closes
|
||||
// the window where the auto-seed effect could also fire on a still-empty read).
|
||||
@@ -959,7 +1001,9 @@ function NgDataProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
export function FestipodDataProvider({ children }: { children: ReactNode }) {
|
||||
const { status } = useNextGraph();
|
||||
console.log('[FestipodData] Provider — NG status:', status);
|
||||
// No identity resolved at this level (only NG connection status is known here) —
|
||||
// identity-first prefix falls back to the bare `[app][data]` form.
|
||||
console.log('[app][data] Provider — NG status:', status);
|
||||
|
||||
if (status === 'connected') {
|
||||
return <NgDataProvider>{children}</NgDataProvider>;
|
||||
|
||||
@@ -111,9 +111,11 @@ export function useShapeQuery<T = UnionSubject>(
|
||||
// "readDoc → N rows" logs gain an app-level running total.
|
||||
recordSet(label, n);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[FestipodData] set reçu: ${n} objets ${label} (${scope}) en ${elapsed}ms`);
|
||||
// No identity in scope at this call site (this hook receives no currentUserId) —
|
||||
// identity-first prefix falls back to the bare `[app][data]` form.
|
||||
console.log(`[app][data] set reçu: ${n} objets ${label} (${scope}) en ${elapsed}ms`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[FestipodData] totaux — ${totalsSummary()} (${totalSets()} sets reçus)`);
|
||||
console.log(`[app][data] totaux — ${totalsSummary()} (${totalSets()} sets reçus)`);
|
||||
}, [query, cycleId, shapeKey, scope]);
|
||||
|
||||
return query;
|
||||
|
||||
Reference in New Issue
Block a user