style(13-03): apply Prettier formatting across repo

Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
Lucas Berger
2026-06-11 20:35:18 -04:00
parent 4bc0445173
commit 982438dc10
398 changed files with 19050 additions and 16382 deletions
@@ -8,20 +8,20 @@
## File Classification
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|-------------------|------|-----------|----------------|---------------|
| `apps/api/src/db/schema.ts` | model | CRUD | `apps/api/src/db/schema.ts` (extend existing) | exact |
| `apps/api/src/broker/write.ts` | service | request-response | `apps/api/src/broker/client.ts` | role-match |
| `apps/api/src/broker/vevent.ts` | utility | transform | `apps/api/src/broker/sync.ts` (ical.js usage) | role-match |
| `apps/api/src/broker/outboxWorker.ts` | service | batch | `apps/api/src/broker/poller.ts` | exact |
| `apps/api/src/routes/events.ts` | route | request-response | `apps/api/src/routes/events.ts` (extend existing) | exact |
| `apps/pwa/src/components/EventDetailPopover.tsx` | component | request-response | `apps/pwa/src/components/EventDetailPopover.tsx` (extend) | exact |
| `apps/pwa/src/components/EventForm.tsx` | component | request-response | `apps/pwa/src/components/EventDetailPopover.tsx` | role-match |
| `apps/pwa/src/components/InstallPrompt.tsx` | component | event-driven | `apps/pwa/src/components/EmptyState.tsx` | partial |
| `apps/pwa/src/api/client.ts` | utility | request-response | `apps/pwa/src/api/client.ts` (extend existing) | exact |
| `apps/pwa/vite.config.ts` | config | — | `apps/pwa/vite.config.ts` (extend existing) | exact |
| `apps/api/tests/broker/outboxWorker.test.ts` | test | batch | `apps/api/tests/broker/sync.test.ts` | role-match |
| `apps/api/tests/routes/events.test.ts` | test | request-response | `apps/api/tests/routes/events.test.ts` (extend) | exact |
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
| ------------------------------------------------ | --------- | ---------------- | --------------------------------------------------------- | ------------- |
| `apps/api/src/db/schema.ts` | model | CRUD | `apps/api/src/db/schema.ts` (extend existing) | exact |
| `apps/api/src/broker/write.ts` | service | request-response | `apps/api/src/broker/client.ts` | role-match |
| `apps/api/src/broker/vevent.ts` | utility | transform | `apps/api/src/broker/sync.ts` (ical.js usage) | role-match |
| `apps/api/src/broker/outboxWorker.ts` | service | batch | `apps/api/src/broker/poller.ts` | exact |
| `apps/api/src/routes/events.ts` | route | request-response | `apps/api/src/routes/events.ts` (extend existing) | exact |
| `apps/pwa/src/components/EventDetailPopover.tsx` | component | request-response | `apps/pwa/src/components/EventDetailPopover.tsx` (extend) | exact |
| `apps/pwa/src/components/EventForm.tsx` | component | request-response | `apps/pwa/src/components/EventDetailPopover.tsx` | role-match |
| `apps/pwa/src/components/InstallPrompt.tsx` | component | event-driven | `apps/pwa/src/components/EmptyState.tsx` | partial |
| `apps/pwa/src/api/client.ts` | utility | request-response | `apps/pwa/src/api/client.ts` (extend existing) | exact |
| `apps/pwa/vite.config.ts` | config | — | `apps/pwa/vite.config.ts` (extend existing) | exact |
| `apps/api/tests/broker/outboxWorker.test.ts` | test | batch | `apps/api/tests/broker/sync.test.ts` | role-match |
| `apps/api/tests/routes/events.test.ts` | test | request-response | `apps/api/tests/routes/events.test.ts` (extend) | exact |
---
@@ -32,6 +32,7 @@
**Analog:** `apps/api/src/db/schema.ts` (lines 1112, existing file)
**Imports pattern** (lines 112):
```typescript
import {
mysqlTable,
@@ -43,11 +44,13 @@ import {
boolean,
index,
unique,
} from 'drizzle-orm/mysql-core'
} from 'drizzle-orm/mysql-core';
```
Add `mysqlEnum` to the import list — already used in the research pattern but not yet in schema.ts.
**Existing table pattern** (lines 86112) — copy this structure for `calendarOutbox`:
```typescript
export const calendarEvents = mysqlTable(
'calendar_events',
@@ -65,15 +68,17 @@ export const calendarEvents = mysqlTable(
index('idx_calendar_events_dtstart_utc').on(t.dtstartUtc),
unique('uniq_calendar_uid').on(t.calendarId, t.uid),
],
)
);
```
**New column on `calendarEvents`** — add `objectUrl` after `etag`:
```typescript
objectUrl: varchar('object_url', { length: 1024 }), // CalDAV object URL; populated by sync.ts from obj.url
```
**References pattern** (lines 4047) — copy for `calendarOutbox.userId`:
```typescript
userId: int('user_id')
.notNull()
@@ -87,18 +92,20 @@ userId: int('user_id')
**Analog:** `apps/api/src/broker/client.ts` (lines 132)
**File header and imports pattern** (client.ts lines 112):
```typescript
/**
* [JSDoc comment with source citations]
* Source: https://...
*/
import { createDAVClient } from 'tsdav'
import { createDAVClient } from 'tsdav';
export type FastmailClient = Awaited<ReturnType<typeof createDAVClient>>
export type FastmailClient = Awaited<ReturnType<typeof createDAVClient>>;
```
**Export pattern** — named exports, no default (matches all broker files):
```typescript
import type { FastmailClient } from './client.js'
import type { DAVCalendar } from 'tsdav'
@@ -117,33 +124,37 @@ export async function deleteCalendarEvent(...): Promise<Response> { ... }
**Analog:** `apps/api/src/broker/sync.ts` (lines 1127) — existing ical.js usage
**ical.js import pattern** (sync.ts line 20):
```typescript
import ICAL from 'ical.js'
import ICAL from 'ical.js';
```
**ical.js parse → component pattern** (sync.ts lines 7286) — the reverse direction (build vs parse) uses the same ICAL.Component/ICAL.Time API:
```typescript
const comp = new ICAL.Component(parsed)
const vevent = comp.getFirstSubcomponent('vevent')
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time | null
const comp = new ICAL.Component(parsed);
const vevent = comp.getFirstSubcomponent('vevent');
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time | null;
```
**D-13 all-day vs timed split** (sync.ts lines 89101) — must mirror this exact split in the builder:
```typescript
// D-13 / Pitfall #3: isDate=true → DATE column; isDate=false → TIMESTAMP column
const allDay: boolean = dtstart?.isDate ?? false
const allDay: boolean = dtstart?.isDate ?? false;
const dtstartDateValue: Date | null =
allDay && dtstart ? new Date(dtstart.toString().slice(0, 10) + 'T00:00:00Z') : null
const dtstartUtcValue: Date | null = !allDay && dtstart ? dtstart.toJSDate() : null
allDay && dtstart ? new Date(dtstart.toString().slice(0, 10) + 'T00:00:00Z') : null;
const dtstartUtcValue: Date | null = !allDay && dtstart ? dtstart.toJSDate() : null;
```
**Error isolation pattern** (sync.ts lines 7478):
```typescript
try {
parsed = ICAL.parse(obj.data as string)
parsed = ICAL.parse(obj.data as string);
} catch {
// Malformed VCALENDAR — skip but do not crash the sync
continue
continue;
}
```
@@ -154,6 +165,7 @@ try {
**Analog:** `apps/api/src/broker/poller.ts` (lines 185) — closest match, exact role
**File header JSDoc pattern** (poller.ts lines 116):
```typescript
/**
* CalDAV broker poller — runs every 5 minutes via node-cron.
@@ -169,18 +181,21 @@ try {
```
**Imports pattern** (poller.ts lines 1825):
```typescript
import { schedule } from 'node-cron'
import { eq } from 'drizzle-orm'
import { db } from '../db/client.js'
import { memberCredentials, calendars } from '../db/schema.js'
import { decryptPassword } from './crypto.js'
import { createFastmailClient } from './client.js'
import { syncCalendar } from './sync.js'
import { schedule } from 'node-cron';
import { eq } from 'drizzle-orm';
import { db } from '../db/client.js';
import { memberCredentials, calendars } from '../db/schema.js';
import { decryptPassword } from './crypto.js';
import { createFastmailClient } from './client.js';
import { syncCalendar } from './sync.js';
```
Replace with: `and`, `lte`, `eq` from `drizzle-orm`; `calendarOutbox`, `calendars` from schema; `syncCalendar` from `./sync.js`; write functions from `./write.js`.
**Exported runX + startX pair pattern** (poller.ts lines 3585):
```typescript
// runPoll exported for unit testing
export async function runPoll(): Promise<void> { ... }
@@ -194,9 +209,11 @@ export function startBrokerPoller(): void {
})
}
```
Outbox worker follows: `export async function runOutboxDrain()` + `export function startOutboxWorker()`.
**Per-item error isolation pattern** (poller.ts lines 6572):
```typescript
} catch (err) {
// Log the error but do NOT log the app password or key (T-03-04)
@@ -208,19 +225,15 @@ Outbox worker follows: `export async function runOutboxDrain()` + `export functi
```
**Drizzle select + where + limit pattern** (poller.ts lines 4753):
```typescript
const [stored] = await db
.select()
.from(calendars)
.where(eq(calendars.url, davCal.url))
.limit(1)
const [stored] = await db.select().from(calendars).where(eq(calendars.url, davCal.url)).limit(1);
```
**Drizzle update pattern** — extend from sync.ts `onDuplicateKeyUpdate` shape:
```typescript
await db.update(calendarOutbox)
.set({ status: 'done' })
.where(eq(calendarOutbox.id, row.id))
await db.update(calendarOutbox).set({ status: 'done' }).where(eq(calendarOutbox.id, row.id));
```
---
@@ -230,6 +243,7 @@ await db.update(calendarOutbox)
**Analog:** `apps/api/src/routes/events.ts` (lines 1141, existing file)
**File header invariant comment** (lines 115) — copy verbatim and extend:
```typescript
/**
* Architecture invariant (T-03-02, broker-boundary):
@@ -240,24 +254,26 @@ await db.update(calendarOutbox)
```
**Hono router + zValidator pattern** (lines 1741):
```typescript
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { and, or, eq, lte, lt } from 'drizzle-orm'
import { sql } from 'drizzle-orm'
import { db } from '../db/client.js'
import { calendarEvents, calendars, users } from '../db/schema.js'
export const eventsRouter = new Hono()
```typescript
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { and, or, eq, lte, lt } from 'drizzle-orm';
import { sql } from 'drizzle-orm';
import { db } from '../db/client.js';
import { calendarEvents, calendars, users } from '../db/schema.js';
export const eventsRouter = new Hono();
const eventsQuerySchema = z.object({
start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
end: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
})
});
```
**Route handler + zValidator + try/catch error pattern** (lines 53141):
```typescript
eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
// ... input validation ...
@@ -270,17 +286,19 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
}
})
```
New write endpoints follow the same shape: `eventsRouter.post('/create', zValidator('json', createSchema), async (c) => { ... })`.
**Auth identity pattern** (from me.ts lines 3344) — write endpoints need current user:
```typescript
const devUser = c.get('user')
const devUser = c.get('user');
if (devUser) {
// dev bypass path
}
const auth = await getAuth(c)
const auth = await getAuth(c);
if (!auth) {
return c.json({ error: 'Unauthorized' }, 401)
return c.json({ error: 'Unauthorized' }, 401);
}
```
@@ -291,19 +309,24 @@ if (!auth) {
**Analog:** `apps/pwa/src/components/EventDetailPopover.tsx` (lines 380388, reserved footer)
**Reserved footer (lines 380388)** — Phase 3 wires buttons here:
```tsx
{/* Phase 3 footer action area — Phase 3 adds edit/delete actions here (D-08) */}
{
/* Phase 3 footer action area — Phase 3 adds edit/delete actions here (D-08) */
}
<div
aria-hidden="true"
style={{
// Reserved: empty in Phase 2 (read-only); Phase 3 wires edit/delete buttons here
marginTop: 'var(--space-4)',
}}
/>
/>;
```
Replace with real content. Remove `aria-hidden="true"`.
**Button style pattern** (lines 235251) — copy close button style for action buttons:
```tsx
<button
aria-label="Close"
@@ -326,25 +349,33 @@ Replace with real content. Remove `aria-hidden="true"`.
```
**Design token usage** — all spacing/color uses CSS vars (not hardcoded values):
- `var(--color-surface-raised)`, `var(--color-text-primary)`, `var(--color-text-secondary)`, `var(--color-border-subtle)`
- `var(--space-2)`, `var(--space-3)`, `var(--space-4)`, `var(--space-6)`
- `var(--text-body-size)`, `var(--text-heading-size)`, `var(--font-family-base)`
**XSS guard pattern** (T-02e-01, lines 283285) — all text content as plain JSX children:
```tsx
{/* Plain text child only — XSS guard (T-02e-01) */}
{occurrence.title}
{
/* Plain text child only — XSS guard (T-02e-01) */
}
{
occurrence.title;
}
```
EventForm must follow this: all field values rendered as plain-text children, never `dangerouslySetInnerHTML`.
**Zustand + TanStack Query pattern** (lines 109137):
```tsx
const { openEventId, setOpenEventId } = useCalendarStore()
const queryClient = useQueryClient()
const { openEventId, setOpenEventId } = useCalendarStore();
const queryClient = useQueryClient();
// Read from TanStack Query cache — do not store server data in Zustand
const allEntries = queryClient.getQueriesData<{ occurrences: CalendarOccurrence[] }>({
queryKey: ['events'],
})
});
```
---
@@ -354,6 +385,7 @@ const allEntries = queryClient.getQueriesData<{ occurrences: CalendarOccurrence[
**Analog:** `apps/pwa/src/components/EventDetailPopover.tsx` (role-match — same overlay surface)
**Modal/overlay structure** — copy the backdrop + dialog pattern from EventDetailPopover (lines 202221):
```tsx
<>
{/* Backdrop */}
@@ -374,26 +406,28 @@ const allEntries = queryClient.getQueriesData<{ occurrences: CalendarOccurrence[
```
**Escape + focus trap useEffect pattern** (lines 143159):
```tsx
useEffect(() => {
if (!activeId) return
if (!activeId) return;
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') handleClose()
}
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [activeId])
if (e.key === 'Escape') handleClose();
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [activeId]);
useEffect(() => {
if (activeId && dialogRef.current) dialogRef.current.focus()
}, [activeId])
if (activeId && dialogRef.current) dialogRef.current.focus();
}, [activeId]);
```
**Responsive phone/desktop detection** (lines 165199) — copy the `isPhone` / `dialogStyle` pattern.
**TanStack Query mutation pattern** — use `useMutation` from `@tanstack/react-query` (same import, already in stack):
```tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useMutation, useQueryClient } from '@tanstack/react-query';
// On success: queryClient.invalidateQueries({ queryKey: ['events'] })
```
@@ -404,6 +438,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
**Analog:** `apps/pwa/src/components/EmptyState.tsx` (partial — informational UI surface)
No close analog. Use the design token and component conventions from EventDetailPopover:
- CSS vars for all spacing/color
- Plain-text JSX children (no dangerouslySetInnerHTML)
- 44px minimum touch targets on all buttons
@@ -418,18 +453,21 @@ No close analog. Use the design token and component conventions from EventDetail
**Analog:** `apps/pwa/src/api/client.ts` (lines 1106, extend)
**Fetch function pattern** (lines 89102):
```typescript
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}`)
throw new Error(`GET /api/events failed: ${res.status}`);
}
return res.json() as Promise<OccurrencesResponse>
return res.json() as Promise<OccurrencesResponse>;
}
```
New write functions follow the same shape. POST/PATCH/DELETE calls:
```typescript
export async function createEvent(payload: CreateEventPayload): Promise<CreateEventResponse> {
const res = await fetch('/api/events/create', {
@@ -437,9 +475,9 @@ export async function createEvent(payload: CreateEventPayload): Promise<CreateEv
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload),
})
if (!res.ok) throw new Error(`POST /api/events/create failed: ${res.status}`)
return res.json() as Promise<CreateEventResponse>
});
if (!res.ok) throw new Error(`POST /api/events/create failed: ${res.status}`);
return res.json() as Promise<CreateEventResponse>;
}
```
@@ -452,9 +490,10 @@ export async function createEvent(payload: CreateEventPayload): Promise<CreateEv
**Analog:** `apps/pwa/vite.config.ts` (lines 113, extend existing)
**Existing config** (lines 113):
```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
@@ -465,8 +504,9 @@ export default defineConfig({
'/callback': 'http://localhost:3000',
},
},
})
});
```
Keep the proxy block exactly as-is. Add `VitePWA` to `plugins` array. The `/callback` proxy entry is critical — it must remain so the SW denylist matches the actual handler.
---
@@ -474,87 +514,103 @@ Keep the proxy block exactly as-is. Add `VitePWA` to `plugins` array. The `/call
## Shared Patterns
### Auth guard in write route handlers
**Source:** `apps/api/src/routes/me.ts` lines 2949
**Apply to:** All new POST/PATCH/DELETE handlers in `routes/events.ts`
```typescript
const devUser = c.get('user')
const devUser = c.get('user');
if (devUser) {
// dev bypass — use devUser.id as userId
}
const auth = await getAuth(c)
const auth = await getAuth(c);
if (!auth) {
return c.json({ error: 'Unauthorized' }, 401)
return c.json({ error: 'Unauthorized' }, 401);
}
```
Also import `'../auth/devBypass.js'` as a side-effect to get the ContextVariableMap augmentation (see me.ts line 25).
### Error handling in route handlers
**Source:** `apps/api/src/routes/events.ts` lines 136140
**Apply to:** All route handlers
```typescript
} catch (err) {
console.error('[events] DB query or expansion failed:', err)
return c.json({ error: 'Service unavailable' }, 503)
}
```
Use consistent `[module/file] description:` log prefix format.
### ESM import extension
**Source:** All existing broker and route files
**Apply to:** All new TypeScript files
All project imports use `.js` extension suffix on relative imports:
`'./client.js'`, `'../db/client.js'`, `'../db/schema.js'`, `'./sync.js'`
### Drizzle DB mock in tests
**Source:** `apps/api/tests/routes/events.test.ts` lines 2952
**Apply to:** `outboxWorker.test.ts`, extended `events.test.ts`
```typescript
// Chain of mocks matching the Drizzle query builder
const mockWhereFn = vi.fn().mockImplementation(() => Promise.resolve(mockDbRows))
const mockFromFn = vi.fn().mockReturnValue({ where: mockWhereFn })
const mockSelectFn = vi.fn().mockReturnValue({ from: mockFromFn })
const mockWhereFn = vi.fn().mockImplementation(() => Promise.resolve(mockDbRows));
const mockFromFn = vi.fn().mockReturnValue({ where: mockWhereFn });
const mockSelectFn = vi.fn().mockReturnValue({ from: mockFromFn });
vi.mock('../../src/db/client.js', () => ({
db: { select: mockSelectFn, insert: mockInsert, update: mockUpdate },
}))
}));
```
### OIDC mock in tests
**Source:** `apps/api/tests/routes/events.test.ts` lines 2226
**Apply to:** All new route tests
```typescript
vi.mock('@hono/oidc-auth', () => ({
oidcAuthMiddleware: () => async (_c: unknown, next: () => Promise<void>) => next(),
processOAuthCallback: () => async (c: { json: (v: unknown) => unknown }) => c.json({ ok: true }),
getAuth: () => null,
}))
}));
```
### TanStack Query integration in React components
**Source:** `apps/pwa/src/components/EventDetailPopover.tsx` lines 26, 111112
**Apply to:** `EventForm.tsx`, `InstallPrompt.tsx`
```tsx
import { useQueryClient } from '@tanstack/react-query'
import { useQueryClient } from '@tanstack/react-query';
// ...
const queryClient = useQueryClient()
const queryClient = useQueryClient();
// On write success: invalidate events cache
queryClient.invalidateQueries({ queryKey: ['events'] })
queryClient.invalidateQueries({ queryKey: ['events'] });
```
### Zustand UI state (not server state)
**Source:** `apps/pwa/src/components/EventDetailPopover.tsx` lines 109110
**Apply to:** `EventForm.tsx`
```tsx
const { openEventId, setOpenEventId } = useCalendarStore()
const { openEventId, setOpenEventId } = useCalendarStore();
```
EventForm visibility/mode (create vs edit) is UI state → Zustand. Event data is server state → TanStack Query.
---
## No Analog Found
| File | Role | Data Flow | Reason |
|------|------|-----------|--------|
| File | Role | Data Flow | Reason |
| ------------------------------------------------------------- | --------- | ------------ | ---------------------------------------------------------------------------- |
| `apps/pwa/src/components/InstallPrompt.tsx` (iOS walkthrough) | component | event-driven | No precedent for install-prompt or browser-API-driven components in codebase |
---