# Phase 2: Calendar Display - Pattern Map

**Mapped:** 2026-06-04
**Files analyzed:** 17 new/modified files
**Analogs found:** 15 / 17

---

## File Classification

| New/Modified File                                | Role       | Data Flow        | Closest Analog                              | Match Quality |
| ------------------------------------------------ | ---------- | ---------------- | ------------------------------------------- | ------------- |
| `apps/api/src/db/schema.ts`                      | model      | CRUD             | self (modify)                               | exact         |
| `apps/api/src/broker/expand.ts`                  | utility    | transform        | `apps/api/src/broker/sync.ts`               | role-match    |
| `apps/api/src/routes/events.ts`                  | route      | request-response | self (modify) + `apps/api/src/routes/me.ts` | exact         |
| `apps/api/src/auth/devBypass.ts`                 | middleware | request-response | `apps/api/src/auth/middleware.ts`           | role-match    |
| `apps/api/src/index.ts`                          | config     | request-response | self (modify)                               | exact         |
| `apps/api/tests/broker/expand.test.ts`           | test       | transform        | `apps/api/tests/broker/poller.test.ts`      | role-match    |
| `apps/api/tests/routes/events.test.ts`           | test       | request-response | `apps/api/tests/health.test.ts`             | role-match    |
| `apps/pwa/vitest.config.ts`                      | config     | —                | `apps/api/vitest.config.ts`                 | role-match    |
| `apps/pwa/src/styles/tokens.css`                 | utility    | —                | none                                        | no analog     |
| `apps/pwa/src/styles/tokens.ts`                  | utility    | —                | none                                        | no analog     |
| `apps/pwa/src/styles/index.css`                  | utility    | —                | none                                        | no analog     |
| `apps/pwa/src/lib/calendarConfig.ts`             | utility    | transform        | `apps/pwa/src/api/client.ts`                | partial       |
| `apps/pwa/src/lib/hydrateEvents.ts`              | utility    | transform        | `apps/pwa/src/api/client.ts`                | partial       |
| `apps/pwa/src/lib/colorUtils.ts`                 | utility    | transform        | `apps/pwa/src/App.tsx` (ColorSwatch)        | partial       |
| `apps/pwa/src/store/calendarStore.ts`            | store      | event-driven     | none                                        | no analog     |
| `apps/pwa/src/components/CalendarShell.tsx`      | component  | request-response | `apps/pwa/src/App.tsx`                      | role-match    |
| `apps/pwa/src/components/EventDetailPopover.tsx` | component  | request-response | `apps/pwa/src/App.tsx` (MemberBadge)        | partial       |
| `apps/pwa/src/components/AppNav.tsx`             | component  | —                | `apps/pwa/src/App.tsx`                      | partial       |
| `apps/pwa/src/components/ViewToolbar.tsx`        | component  | event-driven     | `apps/pwa/src/App.tsx`                      | partial       |
| `apps/pwa/src/components/ColorLegend.tsx`        | component  | —                | `apps/pwa/src/App.tsx` (MemberBadge)        | partial       |
| `apps/pwa/src/components/SkeletonCalendar.tsx`   | component  | —                | `apps/pwa/src/App.tsx` (loading state)      | partial       |
| `apps/pwa/src/api/client.ts`                     | utility    | request-response | self (modify)                               | exact         |
| `apps/pwa/src/main.tsx`                          | config     | —                | self (modify)                               | exact         |

---

## Pattern Assignments

### `apps/api/src/db/schema.ts` (model — modify existing)

**Analog:** self

**Add to `calendarEvents` table — Drizzle column pattern** (lines 84–105 of current file):

```typescript
// New columns to add — follow existing column declaration style exactly:
hasRrule: boolean('has_rrule').default(false).notNull(),
isShared: boolean('is_shared').default(false).notNull(),  // on calendars table, not calendarEvents

// On calendars table — add alongside existing columns:
isShared: boolean('is_shared').default(false).notNull(),

// Index pattern to copy for hasRrule (copy idx_calendar_events_dtstart_utc style):
index('idx_calendar_events_has_rrule').on(t.hasRrule),
```

**Import pattern** (lines 1–11 of existing schema.ts):

```typescript
import {
  mysqlTable,
  varchar,
  text,
  int,
  date,
  timestamp,
  boolean,
  index,
  unique,
} from 'drizzle-orm/mysql-core';
```

---

### `apps/api/src/broker/expand.ts` (utility, transform — new file)

**Analog:** `apps/api/src/broker/sync.ts`

**Imports pattern** (lines 1–8 of sync.ts):

```typescript
import ICAL from 'ical.js';
import { eq } from 'drizzle-orm';
import { db } from '../db/client.js';
import { calendars, calendarEvents } from '../db/schema.js';
```

**ICAL.parse + Component pipeline pattern** (lines 78–91 of sync.ts):

```typescript
let parsed: ReturnType<typeof ICAL.parse>;
try {
  parsed = ICAL.parse(obj.data as string);
} catch {
  // Malformed VCALENDAR — skip but do not crash the sync
  continue;
}

const comp = new ICAL.Component(parsed);
const vevent = comp.getFirstSubcomponent('vevent');
if (!vevent) continue;

const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time | null;
```

**allDay detection pattern** (lines 88–98 of sync.ts):

```typescript
// D-13 / Pitfall #3: isDate=true → DATE column; isDate=false → TIMESTAMP column
const allDay: boolean = dtstart?.isDate ?? false;
```

**Error handling pattern** (lines 75–78 of sync.ts):

```typescript
try {
  parsed = ICAL.parse(obj.data as string);
} catch {
  continue; // malformed VCALENDAR — skip silently
}
```

**VTIMEZONE registration — must come before RecurExpansion** (from RESEARCH.md Pattern 1):

```typescript
// CRITICAL: Register VTIMEZONE before constructing ICAL.RecurExpansion
for (const vtz of comp.getAllSubcomponents('vtimezone')) {
  const tzid = vtz.getFirstPropertyValue('tzid') as string;
  if (tzid && !ICAL.TimezoneService.has(tzid)) {
    ICAL.TimezoneService.register(tzid, new ICAL.Timezone({ component: vtz, tzid }));
  }
}
```

---

### `apps/api/src/routes/events.ts` (route, request-response — modify existing)

**Analog:** `apps/api/src/routes/me.ts` + current `events.ts`

**Route file structure pattern** (lines 1–29 of me.ts):

```typescript
import { Hono } from 'hono';
import { getAuth } from '../auth/middleware.js';
import { upsertUser } from '../auth/user.js';

export const meRouter = new Hono();

meRouter.get('/', async (c) => {
  const auth = await getAuth(c);
  if (!auth) {
    return c.json({ error: 'Unauthorized' }, 401);
  }
  // ... business logic
  return c.json({ user: { id, displayName, color } });
});
```

**Zod query param validation pattern** — follow `@hono/zod-validator` (from CLAUDE.md stack; no existing example yet — planner must scaffold):

```typescript
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

const eventsQuerySchema = z.object({
  start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  end: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});

eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
  const { start, end } = c.req.valid('query');
  // ...
});
```

**Drizzle join pattern** (from sync.ts lines 59, 99 + schema.ts foreign key pattern):

```typescript
// Pattern: db.select().from(table).where(eq(...)).limit(1)
// For join: db.select().from(calendarEvents)
//   .innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
//   .innerJoin(users, eq(calendars.userId, users.id))
//   .where(...)
```

**Error handling pattern** (lines 16–26 of health.ts):

```typescript
try {
  // ...
  return c.json({ ok: true, db: 'up' });
} catch (err) {
  console.error('[health] DB round-trip failed:', err);
  return c.json({ ok: false, db: 'down' }, 503);
}
```

---

### `apps/api/src/auth/devBypass.ts` (middleware — new file)

**Analog:** `apps/api/src/auth/middleware.ts`

**Middleware export pattern** (lines 24–26 of middleware.ts):

```typescript
// middleware.ts uses re-export; devBypass.ts uses named function export
export { oidcAuthMiddleware, processOAuthCallback, getAuth } from '@hono/oidc-auth';
```

**Hono middleware handler signature** (from Hono docs + RESEARCH.md Pattern 5):

```typescript
import type { MiddlewareHandler } from 'hono';

export function devAuthBypass(): MiddlewareHandler {
  // Hard production guard FIRST — before reading any env var
  if (process.env.NODE_ENV === 'production') {
    return async (_c, next) => next();
  }
  if (process.env.DEV_AUTH_BYPASS !== 'true') {
    return async (_c, next) => next();
  }
  return async (c, next) => {
    c.set('user', DEV_USER);
    await next();
  };
}
```

---

### `apps/api/src/index.ts` (config — modify existing)

**Analog:** self

**Middleware mount order pattern** (lines 14–29 of index.ts):

```typescript
// OIDC callback BEFORE auth guard (T-02-02)
app.get('/callback', (c) => processOAuthCallback(c));

// Unauthenticated routes BEFORE the guard
app.route('/health', healthRouter);

// Auth guard on /api/*
app.use('/api/*', oidcAuthMiddleware());

// Protected routes after guard
app.route('/api/me', meRouter);
app.route('/api/events', eventsRouter);
```

**Dev bypass mount pattern** — devBypass must be mounted BEFORE oidcAuthMiddleware:

```typescript
// In dev: swap oidcAuthMiddleware for devAuthBypass when bypass is active
// The bypass short-circuits the OIDC redirect entirely
app.use('/api/*', devAuthBypass()); // no-op passthrough when NODE_ENV=production or flag not set
app.use('/api/*', oidcAuthMiddleware());
// Note: devAuthBypass sets c.set('user', DEV_USER) so oidcAuthMiddleware is still called
// but getAuth(c) will find the injected user. See RESEARCH.md Pattern 5 for alternate approach.
```

---

### `apps/api/tests/broker/expand.test.ts` (test — new file)

**Analog:** `apps/api/tests/broker/poller.test.ts`

**Test file structure** (lines 1–14 of poller.test.ts):

```typescript
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';

// vi.mock hoisted to module top by Vitest
vi.mock('../../src/broker/sync.js', () => ({
  syncCalendar: mockSyncCalendar,
}));
```

**describe/it/expect pattern** (lines 71–121 of poller.test.ts):

```typescript
describe('broker poller — runPoll', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // reset arrays and mock implementations
  });

  it('skips syncCalendar when ctag is unchanged', async () => {
    const { runPoll } = await import('../../src/broker/poller.js');
    // arrange
    await runPoll();
    // assert
    expect(mockSyncCalendar).not.toHaveBeenCalled();
  });
});
```

**Error resilience test pattern** (lines 176–195 of poller.test.ts):

```typescript
it('handles decryptPassword failure gracefully without crashing the poller', async () => {
  (decryptPassword as Mock).mockImplementationOnce(() => {
    throw new Error('Decryption failed');
  });
  await expect(runPoll()).resolves.not.toThrow();
  expect(mockSyncCalendar).not.toHaveBeenCalled();
});
```

**Fixture files** — create in `apps/api/tests/fixtures/` (new directory):

- `weekly-dst.ics` — weekly RRULE spanning March DST (America/New_York)
- `allday-birthday.ics` — DATE-type annual event, no DTEND
- `exdate-series.ics` — weekly series with one EXDATE

---

### `apps/api/tests/routes/events.test.ts` (test — new file)

**Analog:** `apps/api/tests/health.test.ts`

**Route test pattern** (lines 1–38 of health.test.ts):

```typescript
import { describe, it, expect, vi } from 'vitest';

vi.mock('../src/db/client.js', () => ({
  db: {
    execute: vi.fn().mockResolvedValue([[{ '1': 1 }]]),
  },
}));

describe('GET /health', () => {
  it('returns 200 with { ok: true, db: "up" } when DB round-trip succeeds', async () => {
    const { app } = await import('../src/index.js');
    const res = await app.request('/health');
    expect(res.status).toBe(200);
    const body = (await res.json()) as { ok: boolean; db: string };
    expect(body.ok).toBe(true);
  });
});
```

**app.request() pattern for Hono route tests** — use `app.request('/api/events?start=2026-06-01&end=2026-07-01')` following the same import-in-test pattern.

---

### `apps/pwa/vitest.config.ts` (config — new file)

**Analog:** `apps/api/vitest.config.ts`

```typescript
// Copy this exactly, add jsdom environment for React:
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom', // differs from API (node)
    globals: true,
  },
});
```

---

### `apps/pwa/src/api/client.ts` (utility, request-response — modify existing)

**Analog:** self

**Existing function pattern to copy** (lines 22–34 of client.ts):

```typescript
export async function fetchMe(): Promise<MeResponse> {
  const res = await fetch('/api/me', {
    credentials: 'include',
  });
  if (!res.ok) {
    throw new Error(`GET /api/me failed: ${res.status}`);
  }
  return res.json() as Promise<MeResponse>;
}
```

**New `fetchEvents` must follow same shape:**

```typescript
// Replace the existing fetchEvents (no-window version) with a windowed version:
export interface CalendarOccurrence {
  /* from shared types */
}
export interface OccurrencesResponse {
  occurrences: CalendarOccurrence[];
}

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>;
}
```

---

### `apps/pwa/src/lib/hydrateEvents.ts` (utility, transform — new file)

**Analog:** `apps/pwa/src/api/client.ts` (typed transform pattern)

**Interface definition pattern** (lines 12–16 of client.ts):

```typescript
export interface MeUser {
  id: number;
  displayName: string | null;
  color: string;
}
```

**Function export pattern** (lines 22–34 of client.ts):

```typescript
export async function fetchMe(): Promise<MeResponse> { ... }
// → hydrateEvents follows same: export function hydrateEvents(occurrences: CalendarOccurrence[]): ScheduleXEvent[]
```

**Temporal polyfill import** — must be registered before any Temporal usage:

```typescript
import 'temporal-polyfill/global'; // registers Temporal on globalThis; import in main.tsx first
```

---

### `apps/pwa/src/lib/calendarConfig.ts` (utility, transform — new file)

**Analog:** `apps/pwa/src/api/client.ts` (typed constants + factory function)

**Exported constant pattern** (lines 12–16 of client.ts as reference for typed exports):

```typescript
export const WEEK_START_DAY = 0; // 0 = Sunday; Schedule-X uses 7 = Sunday (translate before passing)
```

**Key translation note** — document inline per RESEARCH.md:

```typescript
// WEEK_START_DAY=0 (JS/date-fns Sunday) → Schedule-X firstDayOfWeek=7 (Temporal Sunday)
const sxFirstDay = WEEK_START_DAY === 0 ? 7 : WEEK_START_DAY;
```

---

### `apps/pwa/src/lib/colorUtils.ts` (utility, transform — new file)

**Analog:** `apps/pwa/src/App.tsx` (ColorSwatch inline style, lines 18–34)

**Color inline style pattern to extend** (lines 18–34 of App.tsx):

```typescript
function ColorSwatch({ color }: { color: string }) {
  return (
    <span
      style={{
        background: color,
        border: '1px solid rgba(0,0,0,0.1)',
      }}
      aria-label={`Color: ${color}`}
    />
  )
}
```

**Target function signatures:**

```typescript
// container = main hex at 15% opacity blended over white
export function hexToContainer(hex: string): string; // returns CSS hex or rgba

// onContainer = main hex darkened 40%
export function hexToOnContainer(hex: string): string;

// convenience: all three for Schedule-X lightColors
export function deriveScheduleXColors(main: string): {
  main: string;
  container: string;
  onContainer: string;
};
```

---

### `apps/pwa/src/store/calendarStore.ts` (store, event-driven — new file)

**No existing Zustand analog in codebase.** Follow RESEARCH.md state contract:

```typescript
// State shape from UI-SPEC § State Management Contract:
interface CalendarStore {
  selectedView: string; // persisted in localStorage per breakpointGroup
  selectedDate: string; // ISO string; not persisted
  openEventId: string | null; // null = popover closed
  calendarRange: { start: string; end: string }; // drives TanStack Query key
  setSelectedView: (view: string) => void;
  setSelectedDate: (date: string) => void;
  setOpenEventId: (id: string | null) => void;
  setCalendarRange: (range: { start: string; end: string }) => void;
}
```

---

### `apps/pwa/src/components/CalendarShell.tsx` (component, request-response — new file)

**Analog:** `apps/pwa/src/App.tsx`

**TanStack Query usage pattern** (lines 58–63 of App.tsx):

```typescript
const meQuery = useQuery({
  queryKey: ['me'],
  queryFn: fetchMe,
  retry: false,
  staleTime: 5 * 60 * 1000,
});
```

**Events query — extend this pattern:**

```typescript
const eventsQuery = useQuery({
  queryKey: ['events', start, end],
  queryFn: () => fetchEvents(start, end),
  retry: 2,
  staleTime: 5 * 60 * 1000,
});
```

**Loading/error conditional render pattern** (lines 77–92 of App.tsx):

```typescript
{meQuery.isLoading && (
  <div style={{ color: '#666', marginBottom: '1rem' }}>Loading...</div>
)}
{meQuery.isError && (
  <div style={{ color: '#991b1b', ... }}>Sign-in required</div>
)}
{meQuery.data && (
  <MemberBadge user={meQuery.data.user} />
)}
```

**Component file structure** (App.tsx overall shape):

- Inline interfaces at top
- Sub-components declared before default export
- Default export is the root component
- No CSS modules yet — inline styles or className with `var(--token)` strings

---

### `apps/pwa/src/components/EventDetailPopover.tsx` (component — new file)

**Analog:** `apps/pwa/src/App.tsx` (MemberBadge component, lines 36–55)

**Component prop interface pattern** (lines 36–38 of App.tsx):

```typescript
function MemberBadge({ user }: { user: MeUser }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', ... }}>
```

**Target interface:**

```typescript
interface EventDetailPopoverProps {
  eventId: string | null; // null = closed
  onClose: () => void;
  // event data resolved from Zustand openEventId → TanStack Query cache lookup
}
```

**Accessibility pattern** from UI-SPEC:

- Focus trap while open; Escape closes
- Close button: `aria-label="Close"`; min 44px touch target
- Never use `dangerouslySetInnerHTML` for event title/description (XSS guard)

---

### `apps/pwa/src/components/SkeletonCalendar.tsx` (component — new file)

**Analog:** `apps/pwa/src/App.tsx` loading state (lines 77–80)

**Loading pattern to replace:**

```typescript
{meQuery.isLoading && (
  <div style={{ color: '#666', marginBottom: '1rem' }}>Loading...</div>
)}
```

**Skeleton shimmer approach** — CSS animation, no third-party library:

```css
/* In tokens.css or inline: */
@keyframes shimmer {
  0% {
    background-position: -200% 0;
  }
  100% {
    background-position: 200% 0;
  }
}
/* Apply: background: linear-gradient(90deg, var(--color-surface-dim), var(--color-border-subtle), var(--color-surface-dim));
   background-size: 200% 100%; animation: shimmer 1.5s infinite; */
```

**aria-busy pattern** per UI-SPEC:

```tsx
<div aria-busy="true" aria-label="Loading calendar">
  {/* shimmer placeholders */}
</div>
```

---

### `apps/pwa/src/main.tsx` (config — modify existing)

**Analog:** self

**Current structure** (lines 1–21 of main.tsx):

```typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App.js'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 1,
      staleTime: 30_000,
    },
  },
})

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
)
```

**Add before all other imports** (Temporal polyfill must be first):

```typescript
import 'temporal-polyfill/global'; // registers Temporal on globalThis FIRST
import '@schedule-x/theme-default/dist/index.css'; // Schedule-X layout engine CSS
import './styles/tokens.css'; // token overrides (must come after SX CSS)
```

---

## Shared Patterns

### Authentication Guard (all API routes)

**Source:** `apps/api/src/index.ts` lines 24–29

```typescript
app.use('/api/*', oidcAuthMiddleware());
app.route('/api/me', meRouter);
app.route('/api/events', eventsRouter);
```

**Apply to:** All new/modified route files. Dev bypass mounts before this, not instead.

### Hono Route Error Handling

**Source:** `apps/api/src/routes/health.ts` lines 16–26

```typescript
try {
  await db.execute(sql`SELECT 1`);
  return c.json({ ok: true, db: 'up' });
} catch (err) {
  console.error('[health] DB round-trip failed:', err);
  return c.json({ ok: false, db: 'down' }, 503);
}
```

**Apply to:** `routes/events.ts` — wrap the windowed query + expansion in try/catch, return 503 on DB error.

### Drizzle Upsert Pattern

**Source:** `apps/api/src/broker/sync.ts` lines 39–56

```typescript
await db
  .insert(calendars)
  .values({ ... })
  .onDuplicateKeyUpdate({ set: { ... } })
```

**Apply to:** Any schema migration that adds columns — upsert pattern unchanged.

### D-13 allDay Discrimination

**Source:** `apps/api/src/broker/sync.ts` lines 88–98

```typescript
const allDay: boolean = dtstart?.isDate ?? false;
// dtstartDate: for all-day, convert YYYY-MM-DD → Date at midnight UTC
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;
```

**Apply to:** `broker/expand.ts` — preserve the same discrimination when building CalendarOccurrence output. All-day `start` field must be `'YYYY-MM-DD'` (not a datetime string). Timed `start` must be a timezone-offset ISO string.

### TanStack Query Usage

**Source:** `apps/pwa/src/App.tsx` lines 58–70

```typescript
const meQuery = useQuery({
  queryKey: ['me'],
  queryFn: fetchMe,
  retry: false,
  staleTime: 5 * 60 * 1000,
});
```

**Apply to:** All data-fetching components. Events query uses `retry: 2`. Server data never enters Zustand.

### Fetch Client with Credentials

**Source:** `apps/pwa/src/api/client.ts` lines 22–34

```typescript
const res = await fetch('/api/me', { credentials: 'include' });
if (!res.ok) {
  throw new Error(`GET /api/me failed: ${res.status}`);
}
return res.json() as Promise<MeResponse>;
```

**Apply to:** All new `client.ts` functions (`fetchEvents`). The `credentials: 'include'` is required for the OIDC session cookie.

### CSS Token Usage in Components

**Source:** `apps/pwa/src/App.tsx` lines 37–55 (inline style approach)

```typescript
style={{
  background: '#f0f9ff',       // ← Phase 1: hardcoded
  border: `2px solid ${user.color}`,
}}
```

**Apply to (Phase 2 rule):** Replace all hardcoded hex/px values with `var(--token-name)` CSS custom properties. The existing App.tsx hardcoded values must also be migrated. No hardcoded colors in any Phase 2 component.

---

## No Analog Found

| File                                  | Role    | Data Flow    | Reason                                                        |
| ------------------------------------- | ------- | ------------ | ------------------------------------------------------------- |
| `apps/pwa/src/styles/tokens.css`      | utility | —            | No CSS token layer exists; Phase 2 introduces it from scratch |
| `apps/pwa/src/styles/tokens.ts`       | utility | —            | No TypeScript token mirror exists                             |
| `apps/pwa/src/styles/index.css`       | utility | —            | No global CSS exists; current App.tsx uses inline styles only |
| `apps/pwa/src/store/calendarStore.ts` | store   | event-driven | No Zustand store exists in codebase yet; first Zustand usage  |

---

## Metadata

**Analog search scope:** `apps/api/src/`, `apps/api/tests/`, `apps/pwa/src/`
**Files read:** 15
**Pattern extraction date:** 2026-06-04
