import { useCallback, createContext, useContext, ReactNode } from 'react'; import { AuditEvent, Bundle } from 'fhir/r4'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; import { auditEventSearchParams, type AuditEventQuery } from '../utils/auditEventQuery'; import { searchPageSize } from '../utils/pageSizeOverride'; /* * THE FOUNDATION GATEWAY, NOT THE ROOT API - this asked the wrong host and the page never loaded. * * `GET /R4/AuditEvent` and `/R4/AuditEvent/{id}` are declared in templates/fhir/01-fhir-foundation.yaml * on that stack's own Api, so they are served at fhir-foundation. This module asked `environment.apiUrl` * - the ROOT api - where the path does not exist, and the browser reported net::ERR_FAILED with no status * at all: a response carrying no CORS headers is indistinguishable from a network failure, so the screen * said "No audit events found" and nothing anywhere said the request had not been answered. * * `environment.apiUrl` is for the root api's own concerns - Stripe and service-categories. Every other * foundation-stack resource already reads `fhirFoundationApi`: Consent, DocumentReference, * OrgAuthorization. This was the one that did not. * * MEASURED on the staff portal 2026-08-30 as an Admin whose token carried user/AuditEvent.read, the * Admins group, the org header and a valid DPoP proof - so nothing about the caller was wrong. */ const API_URL = environment.fhirFoundationApi; export const useAuditEventService = () => { const { get } = useHttpService(); /* * ONE PAGE, NARROWED AT THE SERVER - and the previous version's reasoning was right about the wrong * resource. * * It fetched EVERY page before returning, on the argument that a short read is itself an audit * finding: "we looked and there was nothing" is what this screen exists to say, and a page boundary * would make that a lie. True of a NARROWED question. Applied to the unnarrowed one it did the * opposite - 45 sequential requests, ~20 seconds on a blank screen, and 5000 rows of which every * single one was from today, because a day of reads fills the window. The previous day was not * filtered out; it was never reachable. Measured 2026-08-31. * * So the honesty argument now lives where it works: the caller asks a NARROWED question, the server * answers it, and "there was nothing" means nothing matched rather than nothing fit in the window. * The `next` cursor is returned so the page can offer more rather than deciding for the reader. */ const fetchAuditEvents = useCallback(async ( query: Omit = {}, request: { signal?: AbortSignal; admit?: () => void } = {}, ): Promise> => { const search = auditEventSearchParams({ ...query, count: Number(searchPageSize('100')) }); // SEARCH-BOUNDED: this read is deliberately ONE page, and sweeping every page is what it was // fixed from. The paging guard exists so a short read cannot be misread as a short ANSWER - // 'we looked and found nothing' has to mean nothing was recorded. Sweeping every page did not // give that here, it took it away: the audit table only ever grows, so the sweep returned the // newest 5000 rows - all of them from today, measured 2026-08-31 - and everything older was // silently unreachable. The completeness now comes from the QUESTION being narrowed at the // server, plus a `next` cursor handed back so the reader continues rather than the client // deciding for them. An empty result now means nothing matched. return get>(`${API_URL}/AuditEvent?${search}`, request); }, [get]); const fetchAuditEvent = useCallback(async (id: string): Promise => { return get(`${API_URL}/AuditEvent/${id}`); }, [get]); return { fetchAuditEvents, fetchAuditEvent }; }; const AuditEventServiceContext = createContext | undefined>(undefined); export const AuditEventServiceProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const auditEventService = useAuditEventService(); return ( {children} ); }; export const useAuditEventServiceContext = () => { const context = useContext(AuditEventServiceContext); if (context === undefined) { throw new Error('useAuditEventServiceContext must be used within an AuditEventServiceProvider'); } return context; };