feat(02-03): hydrateEvents + calendarStore + windowed fetchEvents; RED stubs green
- Create hydrateEvents.ts: Temporal.PlainDate for allDay, ZonedDateTime for timed calendarId routes via isShared ? 'shared' : String(ownerUserId) — NOT String(calendarId) _familySync carries uid/color/isShared through to popover - Update hydrateEvents.test.ts: add temporal-polyfill/global import; all 4 RED stubs now GREEN - Create calendarStore.ts: Zustand store with selectedView (localStorage per breakpoint group), selectedDate, openEventId, calendarRange; D-05 view defaults; calendarRange ± buffer for initial TanStack Query key without depending on onRangeUpdate firing on mount - Update client.ts: add CalendarOccurrence/OccurrencesResponse, windowed fetchEvents(start,end) with credentials:include; keep legacy CalendarEvent/EventsResponse + fetchEventsLegacy as deprecated for EventProof.tsx (removed in Plan 05) - Update EventProof.tsx: switch to fetchEventsLegacy to keep build clean until Plan 05 - tsc --noEmit clean; all 18 PWA tests pass
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
* 302 redirect to Authelia on the next API call automatically (full-page nav).
|
||||
*/
|
||||
|
||||
// ── /api/me ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface MeUser {
|
||||
id: number
|
||||
displayName: string | null
|
||||
@@ -33,9 +35,73 @@ export async function fetchMe(): Promise<MeResponse> {
|
||||
return res.json() as Promise<MeResponse>
|
||||
}
|
||||
|
||||
// ── /api/events (windowed — Phase 2) ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* A single cached calendar event from the broker's MariaDB cache.
|
||||
* Mirrors the calendarEvents table schema from apps/api/src/db/schema.ts.
|
||||
* A single concrete occurrence of a calendar event.
|
||||
*
|
||||
* Mirrors the CalendarOccurrence shape produced by the backend's
|
||||
* expandOccurrences() helper (apps/api/src/broker/expand.ts).
|
||||
*
|
||||
* calendarId and ownerUserId are both present:
|
||||
* calendarId — DB calendar-row id (do NOT use for Schedule-X routing)
|
||||
* ownerUserId — DB user id (use for Schedule-X calendarId routing)
|
||||
* isShared — true when this event belongs to the shared-family calendar
|
||||
*
|
||||
* hydrateEvents() uses isShared/ownerUserId — never String(calendarId) —
|
||||
* to build the Schedule-X calendarId that keys into buildCalendarConfig().
|
||||
*/
|
||||
export interface CalendarOccurrence {
|
||||
id: string // `${uid}::${dtstart_iso}` — stable identity
|
||||
uid: string
|
||||
calendarId: number // DB calendar-row id — do NOT use for SX calendarId routing
|
||||
calendarName: string
|
||||
ownerUserId: number // DB user id — the correct Schedule-X routing key
|
||||
color: string // hex from users.color or shared-family constant
|
||||
isShared: boolean // true → 'shared' slot; false → String(ownerUserId) slot
|
||||
title: string
|
||||
start: string // 'YYYY-MM-DD' for allDay:true; ISO 8601 with IANA tz for timed
|
||||
end: string
|
||||
allDay: boolean
|
||||
location: string | null
|
||||
description: string | null
|
||||
}
|
||||
|
||||
export interface OccurrencesResponse {
|
||||
occurrences: CalendarOccurrence[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch windowed calendar occurrences.
|
||||
*
|
||||
* The ?start=&end= window is mandatory — an unwindowed call would expand 500+
|
||||
* cached events with all their recurring occurrences (RESEARCH.md Pitfall 5).
|
||||
*
|
||||
* @param start ISO date string 'YYYY-MM-DD' — window start (inclusive)
|
||||
* @param end ISO date string 'YYYY-MM-DD' — window end (exclusive)
|
||||
*/
|
||||
export async function fetchEvents(
|
||||
start: string,
|
||||
end: string,
|
||||
): Promise<OccurrencesResponse> {
|
||||
const res = await fetch(`/api/events?start=${start}&end=${end}`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`GET /api/events failed: ${res.status}`)
|
||||
}
|
||||
|
||||
return res.json() as Promise<OccurrencesResponse>
|
||||
}
|
||||
|
||||
// ── Legacy types + function for EventProof (Phase 1 broker-proof component) ──
|
||||
// EventProof.tsx uses these; the component is removed in Plan 05.
|
||||
// Kept here to prevent build breakage until then.
|
||||
|
||||
/**
|
||||
* @deprecated Phase 1 raw cache shape — use CalendarOccurrence for Phase 2.
|
||||
* EventProof uses this type; it is removed in Plan 05.
|
||||
*/
|
||||
export interface CalendarEvent {
|
||||
id: number
|
||||
@@ -49,24 +115,23 @@ export interface CalendarEvent {
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Phase 1 raw response shape — use OccurrencesResponse for Phase 2.
|
||||
*/
|
||||
export interface EventsResponse {
|
||||
events: CalendarEvent[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all cached calendar events from the broker's MariaDB cache.
|
||||
* No live Fastmail call — the 5-min poller keeps the cache fresh.
|
||||
*
|
||||
* Used by EventProof to display the first cached event as broker proof (CAL-01).
|
||||
* @deprecated Phase 1 broker-proof fetch — unwindowed, returns raw cache rows.
|
||||
* Used only by EventProof.tsx; removed in Plan 05.
|
||||
*/
|
||||
export async function fetchEvents(): Promise<EventsResponse> {
|
||||
export async function fetchEventsLegacy(): Promise<EventsResponse> {
|
||||
const res = await fetch('/api/events', {
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`GET /api/events failed: ${res.status}`)
|
||||
}
|
||||
|
||||
return res.json() as Promise<EventsResponse>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user