- eslint.config.js: disable React Compiler rules (v7 flat.recommended enables
them; codebase does not use the Compiler); add e2e/ to disableTypeChecked
block; promote exhaustive-deps to error
- API broker: remove redundant as-casts (outboxWorker, poller, reminderScheduler,
expand, sync, vevent, spike); add targeted ical.js no-unsafe-assignment/argument
disables with justifying comments inside try blocks
- API routes/sse.ts: fix no-misused-promises on async writeSSE callback with
void+IIFE+catch pattern
- API routes/lists.ts: let → const for updateValues
- API tests: remove unused imports (beforeEach, eq, vi); rename unused vars
with _ prefix; remove unused lastActiveId assignment
- PWA components: void navigate() and void queryClient.invalidateQueries() on
all fire-and-forget call sites; fix CalendarShell explicit-type-casts;
Couldn't → HTML entity
- PWA test files: as unknown as Response for partial mock objects; string | null
type annotation on mockLastSyncedUid; remove async from test callbacks without
await; act(() => {}) not await act(async () => {}) for sync ops
- sw.ts: restructure Notification.data?.url access as let+if so disable
comments land on the exact violation lines; void self.skipWaiting()
- Lift AppNav from CalendarShell to App.tsx as a sibling of <Routes>
- App.tsx fetches /api/me (same query key as CalendarShell — deduplicated by TanStack Query)
- App.tsx provides the outer layout (phone: column, desktop: row) with AppNav always rendered
- CalendarShell simplified: no longer manages AppNav, outer flex layout stays in App.tsx
- AuthSplash gains overlay prop (position:fixed inset:0 z-index:999) so it covers AppNav when needed
- CalendarShell uses AuthSplash with overlay=true so auth splashes cover full viewport
- Remove onOpenSettings prop from CalendarShell (wired directly in App.tsx to SettingsSheet)
- Desktop sidebar nav (FamilySync brand, Calendar/Lists links) now persists on /lists route
- CalendarShell now captures maybeRedirectToLogin() return value in meQuery.isError effect
- When the one-shot guard is exhausted (returns false), arm loginRedirectExhausted state
- Render AuthSplash state=dead-end (tap-to-retry) when guard is exhausted, not indefinite redirecting spinner
- Reset loginRedirectExhausted on successful auth (meQuery.isSuccess) for session recovery
- Add sessionStorage.clear() to beforeEach so CalendarShell tests are isolated
- RED test committed in prior commit (36ef7a0)
- Create PermissionDeniedBanner.tsx: role=alert banner shown only when
permission=denied AND notificationsEnabled=1 (OS-revoked case, D-10)
with AlertCircle icon, 'Notifications blocked' heading, inline 'How to enable'
button that opens OS-specific instruction sheet (iOS 4-step / Android 4-step)
- Mount PermissionDeniedBanner and SettingsSheet in App.tsx; wire onOpenSettings
state from avatar tap through CalendarShell → AppNav → PhoneNav/DesktopNav
- CalendarShell accepts optional onOpenSettings prop, threads to both AppNav usages
- playwright-cli verified: banner renders with exact UI-SPEC copy when
permission=denied+was-enabled; banner absent when permission=granted;
'How to enable' opens instruction sheet with correct Android steps;
SettingsSheet opens from avatar click with toggle + permission-denied hint
The calendar flashed whenever the event popup/form closed or a post-write events
refetch landed. Root cause: CalendarContent was a function component DEFINED
INSIDE CalendarShell's render and used as <CalendarContent />. A nested component
has a new identity every render, so React unmounted+remounted its whole subtree
— including <ScheduleXCalendar> — on ANY CalendarShell re-render. The earlier
Bug B work only minimized re-renders (Zustand selectors) to dodge this; the
resync-before-done fix made the post-write ['events'] refetch deliver changed
data again, so the remount/flash returned.
Fix: render the content as a plain JSX element value (const calendarContent)
referenced at both layout sites instead of a nested component type. Element
values reconcile in place across re-renders — no remount, no flash.
Internal Server Error after returning from Authelia: processOAuthCallback threw
OAUTH_INVALID_RESPONSE ("unexpected state parameter") because the OIDC state
cookie no longer matched the state returned to /callback.
Root cause: eventsQuery (fetchEvents, redirect:'follow', retry:2) ran
concurrently with fetchMe on load. While unauthenticated, every /api/* request
hits the OIDC guard, which 302-redirects to Authelia AND sets a fresh state
cookie. fetchEvents could not follow the cross-origin redirect, so React Query
retried it up to 3x over ~3s — each retry overwriting the state cookie mid-login,
racing the single /api/login navigation that owns the real flow.
Fix: enabled: meQuery.isSuccess. Only fetchMe (redirect:'manual', retry:false)
touches a guarded endpoint while unauthenticated, so the top-level /api/login
navigation owns the state cookie uncontested. Realizes the documented design
intent that only fetchMe drives the login redirect.
- Import maybeRedirectToLogin + clearLoginRedirect from loginRedirect.ts
- useEffect on meQuery.isError calls maybeRedirectToLogin() (one-shot, loop-guarded)
- useEffect on meQuery.isSuccess calls clearLoginRedirect() for future re-auth
- Existing 'Sign-in required' branch retained as fall-through for already-attempted case
- Import EventForm and Plus icon from lucide-react
- Phone: fixed FAB bottom-right (56px, dark neutral fill per UI-SPEC)
- Tablet/desktop: toolbar button above calendar content area
- Both trigger setEventForm(true, 'create') via Zustand
- EventForm conditionally rendered while eventFormOpen
- Selectors pattern preserved to avoid unnecessary re-renders (Bug B guard)
Schedule-X defaults its calendar timezone to 'UTC', so a 17:45-04:00 event rendered at
21:45 (9:45 PM). Set timezone to the viewer's resolved IANA zone so events convert to
local wall-clock; the popover already showed local time, so the two now agree.
Navigation now lives in Schedule-X's built-in header; replacing the calendar with
EmptyState on a zero-event day removed the nav and stranded the user. Always render
the calendar (empty grid is self-explanatory).
- CalendarShell: remove overflow:hidden from calendar container; add
height:100% so Schedule-X .sx__calendar-wrapper can fill the flex
parent and .sx__view-container can scroll.
- index.css: add explicit .sx__calendar-wrapper { height: 100% } rule
to propagate height through the React adapter's wrapper element.
- tokens.css: fix --sx-color-neutral override from near-white
var(--color-surface-dim) to readable var(--color-text-secondary);
fix --sx-color-neutral-variant to var(--color-border); add
--sx-internal-color-text override for chevrons and UI borders.
Both hour-axis labels (.sx__week-grid__hour-text) and weekday/day
name headers (.sx__week-grid__day-name, .sx__week-grid__date-number)
use --sx-color-neutral — all now readable.
Class and variable names confirmed from @schedule-x/theme-default@4.6.0
dist/index.css inspection.
- Remove <ViewToolbar> render and its import from CalendarShell
- Remove createCalendarControlsPlugin import, useState instance, and plugin
array entry (calendar-controls only served the custom toolbar)
- Delete ViewToolbar.tsx (no longer referenced anywhere)
- Remove calendar-controls mock from CalendarShell.test.tsx
- CSS audit confirmed no rules hide Schedule-X weekday-name row; no CSS changes needed
- All four views (day/week/month-grid/month-agenda) remain; Schedule-X's native
header exposes them in its own view switcher
Bug A — navigation no-op: replace $app.calendarState private-API poking
with the official @schedule-x/calendar-controls plugin. CalendarShell
creates the plugin once via useState stable initialiser and passes it to
ViewToolbar as `controls`. ViewToolbar calls controls.setDate(PlainDate)
and controls.setView(id) for all navigation and view-switching. Step size
matches the active view: day→±1 day, week→±1 week, month-*→±1 month.
Bug B — popover-open calendar flash: replace the unselected
useCalendarStore() destructuring in CalendarShell and ViewToolbar with
per-field selectors. Neither component now subscribes to openEventId, so
popover open/close no longer triggers a re-render that rebuilds the
Schedule-X config.
- Add @schedule-x/calendar-controls@4.6.0 dependency
- Update CalendarShell.test.tsx: add vi.mock for calendar-controls
- typecheck, vitest (37/37), build all pass
- BUG 2: onRangeUpdate sets exclusive end = range.end + 1 day so day view
sends a 1-day window (start < end, no 400) and week/month include the last day
- BUG 3: formatDateTime strips IANA bracket '[Zone]' before new Date() to prevent
'Invalid Date, Invalid Date – Invalid Date' in event popover; regression test added
- BUG 4: remove createEventModalPlugin + customComponents.eventModal — keep only
the Zustand-driven standalone EventDetailPopover to prevent double-open fight