From faf44c18fc64edb4219e0b3405e8f4e1510a2e07 Mon Sep 17 00:00:00 2001 From: alvis Date: Thu, 16 Apr 2026 07:44:37 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=CE=B5-greedy=20v1=20as=20active=20poli?= =?UTF-8?q?cy;=20dwell-time=20reward=20inference;=20offline=20sim=20framew?= =?UTF-8?q?ork?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Promote egreedy-v1 to active serving policy (ADR-0007): /score/egreedy + /reward/egreedy replaces linucb-v1 endpoints after offline sim shows +10.7% mean reward (−0.548 vs −0.606) - Replace explicit helpful/not_helpful feedback with dwell-time inferred reward (inferReward): dismiss=−1.0, snooze=+0.1, done<15s=−0.3, done 15s–2min=+1.0, done 2–10min=+0.6, done>10min=+0.3 - Add ml/serving ε-greedy endpoints: /score/egreedy, /reward/egreedy, /stats/egreedy/{user_id} with d=7 feature vector (base 5 + sin/cos day-of-week encoding) - Add offline simulation framework (ml/experiments/sim): rule/LLM/claude-code judges, two-phase score+reward, synthetic personas, task generator; results stored in sim_runs/sim_events - Add /admin/simulations page: start runs, live-poll status, reward curve SVG, action/persona tables - Fix egreedy day_of_week training skew: reward endpoint now uses actual dow instead of hardcoded 0 - Fix runner.py proxy bypass: httpx.Client(trust_env=False) for localhost ML calls - Add dwellMs to TipFeedbackEvent contract and bus.test.ts fixture - Schema: sim_runs, sim_events tables; tip_feedback gains dwell_ms, reward_milli columns - ADR-0006: admin console framework; ADR-0007: egreedy-v1 policy selection rationale Co-Authored-By: Claude Sonnet 4.6 --- README.md | 9 +- apps/admin/README.md | 37 + apps/admin/next.config.ts | 14 + apps/admin/package.json | 32 + apps/admin/postcss.config.js | 6 + apps/admin/src/app/simulations/page.tsx | 499 +++ apps/admin/src/components/AdminShell.tsx | 1 + apps/admin/src/lib/api.ts | 64 + apps/admin/tailwind.config.ts | 12 + apps/admin/tsconfig.json | 23 + apps/admin/tsconfig.tsbuildinfo | 1 + apps/web/e2e/sign-in.spec.ts | 11 + apps/web/package.json | 16 +- apps/web/playwright.config.ts | 24 + .../src/components/__tests__/TipPage.test.tsx | 131 + apps/web/src/test/setup.ts | 1 + apps/web/vitest.config.ts | 23 + docs/adr/0006-admin-console-framework.md | 59 + docs/adr/0007-egreedy-v1-active-policy.md | 47 + infra/ci/ci.yml | 28 + ml/experiments/sim/llm_judge.py | 204 ++ ml/experiments/sim/personas.py | 79 + ml/experiments/sim/runner.py | 527 ++++ ml/experiments/sim/task_generator.py | 62 + ml/serving/main.py | 159 +- ml/serving/package.json | 3 +- ml/serving/requirements-dev.txt | 4 + ml/serving/requirements.txt | 2 + ml/serving/tests/__init__.py | 0 ml/serving/tests/test_score.py | 261 ++ packages/shared-types/package.json | 6 +- .../shared-types/src/__tests__/tip.test.ts | 40 + packages/shared-types/vitest.config.ts | 9 + pnpm-lock.yaml | 2682 ++++++++++++++++- services/api/package.json | 7 +- services/api/src/db/index.ts | 36 + services/api/src/db/schema.ts | 39 + services/api/src/events/__tests__/bus.test.ts | 173 ++ services/api/src/events/bus.ts | 6 +- .../src/middleware/__tests__/admin.test.ts | 109 + services/api/src/middleware/admin.ts | 27 + .../api/src/routes/__tests__/admin.test.ts | 370 +++ services/api/src/routes/admin.ts | 166 + services/api/src/routes/recommender.ts | 80 +- services/api/src/routes/user.ts | 1 + services/api/src/test/db.ts | 84 + services/api/vitest.config.ts | 13 + turbo.json | 4 + 48 files changed, 6151 insertions(+), 40 deletions(-) create mode 100644 apps/admin/README.md create mode 100644 apps/admin/next.config.ts create mode 100644 apps/admin/package.json create mode 100644 apps/admin/postcss.config.js create mode 100644 apps/admin/src/app/simulations/page.tsx create mode 100644 apps/admin/tailwind.config.ts create mode 100644 apps/admin/tsconfig.json create mode 100644 apps/admin/tsconfig.tsbuildinfo create mode 100644 apps/web/e2e/sign-in.spec.ts create mode 100644 apps/web/playwright.config.ts create mode 100644 apps/web/src/components/__tests__/TipPage.test.tsx create mode 100644 apps/web/src/test/setup.ts create mode 100644 apps/web/vitest.config.ts create mode 100644 docs/adr/0006-admin-console-framework.md create mode 100644 docs/adr/0007-egreedy-v1-active-policy.md create mode 100644 ml/experiments/sim/llm_judge.py create mode 100644 ml/experiments/sim/personas.py create mode 100644 ml/experiments/sim/runner.py create mode 100644 ml/experiments/sim/task_generator.py create mode 100644 ml/serving/requirements-dev.txt create mode 100644 ml/serving/tests/__init__.py create mode 100644 ml/serving/tests/test_score.py create mode 100644 packages/shared-types/src/__tests__/tip.test.ts create mode 100644 packages/shared-types/vitest.config.ts create mode 100644 services/api/src/events/__tests__/bus.test.ts create mode 100644 services/api/src/middleware/__tests__/admin.test.ts create mode 100644 services/api/src/middleware/admin.ts create mode 100644 services/api/src/routes/__tests__/admin.test.ts create mode 100644 services/api/src/test/db.ts create mode 100644 services/api/vitest.config.ts diff --git a/README.md b/README.md index 1583dc4..7e54f84 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Goal: a single user signs in with Google, connects Todoist, and sees one random - [x] `integrations/todoist` — OAuth2 flow, token stored in DB, disconnect supported - [x] `recommender` with `RandomPolicy`; stable `POST /recommend` contract; 30s task cache - [x] `apps/web` — sign-in, connect, tip pages; PWA manifest + icons -- [x] Feedback endpoint (done/dismiss/snooze); marks task complete in Todoist +- [x] Feedback: `done / snooze / dismiss`; reward inferred from dwell-time (`inferReward`); marks task complete in Todoist - [x] Deploy modular monolith to Agap VM via Caddy at `o.alogins.net` - [x] ToS + Privacy Policy pages (`/legal/terms`, `/legal/privacy`); implicit consent on sign-in - [x] Account deletion: revokes tokens, purges data, soft-deletes profile; button on /connect @@ -87,10 +87,11 @@ Goal: tips are picked, not drawn from a hat — and they arrive at the right mom - [x] Event bus scaffold: typed in-process EventEmitter with 500-event ring buffer; subjects match future NATS JetStream — swap is mechanical - [x] Todoist sync emits `signals.task.synced`; tip served/feedback emit `signals.tip.*` - [x] Features extracted per task: `is_overdue`, `task_age_days`, `priority`; context: `hour_of_day`, `day_of_week` -- [x] `ml/serving` LinUCB bandit (d=5, alpha=1.0); per-user state persisted to disk; `/score` + `/reward` + `/reset` + `/stats` + `/features` endpoints +- [x] `ml/serving` LinUCB (d=5) + **ε-greedy v1** (d=7, ε=0.10, day-of-week sin/cos features); per-user state persisted to disk - [x] `RemotePolicy` in recommender: calls ml/serving, falls back to RandomPolicy on timeout/error; logs explainability to `tip_scores` -- [x] Feedback loop: reactions mapped to rewards (done=+1, helpful=+0.5, snooze=0, not_helpful=−0.5, dismiss=−1) → online LinUCB update -- [x] In-app **helpful / not helpful** coarse signal (#62) — long-press action sheet on tip page +- [x] Feedback loop: dwell-time inferred reward (`inferReward`) → online model update; `done` in 15 s–2 min = +1.0 (magic zone) +- [x] Offline simulation framework (`ml/experiments/sim`): rule/LLM/claude-code judges, two-policy comparison, results persisted to `sim_runs` + `sim_events` +- [x] **ε-greedy v1 promoted to active policy** (ADR-0007) — +10.7% mean reward vs LinUCB in offline sim - [x] **Web Push** (VAPID): SW, subscribe/unsubscribe API, "notify me" button on tip page - [x] Shadow-policy registry: run N shadow policies per request, log picks without serving them (#56) - [ ] Quiet-hours + dedupe for push delivery diff --git a/apps/admin/README.md b/apps/admin/README.md new file mode 100644 index 0000000..fd6e373 --- /dev/null +++ b/apps/admin/README.md @@ -0,0 +1,37 @@ +# apps/admin — oO Admin Console + +Next.js 15 app. Deployed at `admin.o.alogins.net` (dev: `http://localhost:3080`). + +## Contract + +- All routes are admin-only. The Next.js middleware calls `GET /api/user/me` on every request + and checks `role === 'admin'`. First admin is seeded via `ADMIN_SEED_EMAIL` env var at API startup. +- Admin write actions are appended to the `admin_actions` audit log in the DB. + +## Pages + +| Route | Description | +|-------|-------------| +| `/` | Overview: DAU/WAU KPI cards, tips served, reaction breakdown, activation funnel | +| `/users` | User list (paginated) | +| `/users/:id` | User detail: identity, consents, integrations, tip stats, reward history; revoke-integration + reset-bandit actions | +| `/audit` | Admin action audit log | +| `/events` | Event stream viewer (stub — pending API history endpoint) | + +## Dev + +```bash +pnpm --filter @oo/admin dev # starts on :3080 +# also run the API: pnpm --filter @oo/api dev (port 3078) +``` + +## Extraction criteria + +Stays as a Next.js app in the monorepo permanently — it's not a candidate for extraction. +It gets richer (more pages, embedded MLflow/Grafana) but not split. + +## Known issues + +- `@tremor/react 3.x` declares a peer dep on React 18; the workspace uses React 19. + Works in practice. Will resolve naturally when Tremor ships React 19 support or when + we switch to Tremor v4 (which targets React 18+). diff --git a/apps/admin/next.config.ts b/apps/admin/next.config.ts new file mode 100644 index 0000000..6e87389 --- /dev/null +++ b/apps/admin/next.config.ts @@ -0,0 +1,14 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + async rewrites() { + return [ + { + source: '/api/:path*', + destination: `${process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3078'}/api/:path*`, + }, + ]; + }, +}; + +export default nextConfig; diff --git a/apps/admin/package.json b/apps/admin/package.json new file mode 100644 index 0000000..0e121b7 --- /dev/null +++ b/apps/admin/package.json @@ -0,0 +1,32 @@ +{ + "name": "@oo/admin", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "next dev -p 3080", + "build": "next build", + "start": "next start -p 3080", + "lint": "next lint", + "type-check": "tsc --noEmit", + "clean": "rm -rf .next" + }, + "dependencies": { + "@oo/shared-types": "workspace:*", + "@tremor/react": "^3.18.3", + "@tanstack/react-table": "^8.20.5", + "next": "^15.1.6", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "recharts": "^2.15.3", + "marked": "^14.1.4" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "tailwindcss": "^3.4.17", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", + "typescript": "^5.7.3" + } +} diff --git a/apps/admin/postcss.config.js b/apps/admin/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/apps/admin/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/admin/src/app/simulations/page.tsx b/apps/admin/src/app/simulations/page.tsx new file mode 100644 index 0000000..6a074fc --- /dev/null +++ b/apps/admin/src/app/simulations/page.tsx @@ -0,0 +1,499 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { AdminShell } from '@/components/AdminShell'; +import { + type PolicySummary, + type SimEvent, + type SimRun, + getSimRun, + getSimRuns, + startSimulation, +} from '@/lib/api'; + +const KNOWN_POLICIES = ['linucb-v1', 'egreedy-v1']; +const ACTIONS = ['done', 'snooze', 'dismiss']; +// Shown as reference only — actual reward is dwell-time inferred for 'done' +const ACTION_REWARDS: Record = { + done: 1.0, snooze: 0.1, dismiss: -1.0, +}; + +// ── SVG reward curve ──────────────────────────────────────────────────────── + +function RewardCurve({ summary, policies }: { summary: Record; policies: string[] }) { + const W = 520, H = 160, PAD = { t: 10, r: 10, b: 30, l: 40 }; + const iW = W - PAD.l - PAD.r; + const iH = H - PAD.t - PAD.b; + + const allVals = policies.flatMap((p) => summary[p]?.cumulative_rewards ?? []); + const minY = Math.min(0, ...allVals); + const maxY = Math.max(1, ...allVals); + const n = Math.max(...policies.map((p) => (summary[p]?.cumulative_rewards ?? []).length)); + + const xScale = (i: number) => PAD.l + (i / Math.max(1, n - 1)) * iW; + const yScale = (v: number) => PAD.t + iH - ((v - minY) / (maxY - minY)) * iH; + + const COLORS = ['#818cf8', '#34d399', '#f87171', '#fbbf24']; + + const path = (vals: number[]) => + vals + .map((v, i) => `${i === 0 ? 'M' : 'L'}${xScale(i).toFixed(1)},${yScale(v).toFixed(1)}`) + .join(' '); + + // Axis labels + const yLabels = [minY, (minY + maxY) / 2, maxY]; + + return ( + + {/* Grid */} + {yLabels.map((v, i) => ( + + + + {v.toFixed(1)} + + + ))} + {/* Zero line */} + {minY < 0 && ( + + )} + {/* Curves */} + {policies.map((p, pi) => { + const vals = summary[p]?.cumulative_rewards ?? []; + if (!vals.length) return null; + return ( + + + + + ); + })} + {/* X axis */} + + Round + {/* Legend */} + {policies.map((p, pi) => ( + + + {p} + + ))} + + ); +} + +// ── Action distribution table ─────────────────────────────────────────────── + +function ActionTable({ + summary, + policies, +}: { + summary: Record; + policies: string[]; +}) { + return ( + + + + + {policies.map((p) => ( + + ))} + + + + + {ACTIONS.map((action) => ( + + + {policies.map((p) => { + const n = summary[p]?.action_counts?.[action] ?? 0; + const total = Object.values(summary[p]?.action_counts ?? {}).reduce( + (a, b) => a + b, 0 + ); + const pct = total > 0 ? ((n / total) * 100).toFixed(1) : '—'; + return ( + + ); + })} + + + ))} + +
Action{p}Reward
{action} + {n} ({pct}%) + 0 ? 'text-green-400' : ACTION_REWARDS[action] < 0 ? 'text-red-400' : 'text-gray-500'}`}> + {ACTION_REWARDS[action] >= 0 ? '+' : ''}{ACTION_REWARDS[action]} +
+ ); +} + +// ── Per-persona breakdown ─────────────────────────────────────────────────── + +function PersonaTable({ + breakdown, + policies, +}: { + breakdown: Record>; + policies: string[]; +}) { + const personas = Object.keys(breakdown); + return ( + + + + + {policies.map((p) => ( + + ))} + + + + + {personas.map((persona) => { + const pdata = breakdown[persona]; + const best = policies.reduce((a, b) => + (pdata[a]?.reward ?? -Infinity) >= (pdata[b]?.reward ?? -Infinity) ? a : b + ); + return ( + + + {policies.map((p) => { + const d = pdata[p]; + const mean = d && d.n > 0 ? (d.reward / d.n).toFixed(3) : '—'; + return ( + + ); + })} + + + ); + })} + +
Persona{p}
mean reward
Winner
{persona} + {mean} + {best}
+ ); +} + +// ── Run detail panel ──────────────────────────────────────────────────────── + +function RunDetail({ runId, onClose }: { runId: string; onClose: () => void }) { + const [data, setData] = useState<{ run: SimRun; events: SimEvent[] } | null>(null); + const [error, setError] = useState(''); + const pollRef = useRef | null>(null); + + const load = async () => { + try { + const d = await getSimRun(runId); + setData(d); + if (d.run.status !== 'running' && d.run.status !== 'pending') { + if (pollRef.current) clearInterval(pollRef.current); + } + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Failed to load'); + if (pollRef.current) clearInterval(pollRef.current); + } + }; + + useEffect(() => { + load(); + pollRef.current = setInterval(load, 3000); + return () => { if (pollRef.current) clearInterval(pollRef.current); }; + }, [runId]); + + const run = data?.run; + const summary: Record | null = run?.summaryJson + ? JSON.parse(run.summaryJson) + : null; + const breakdown: Record> | null = + run?.personaBreakdownJson ? JSON.parse(run.personaBreakdownJson) : null; + const policies = run ? [run.policyA, run.policyB] : []; + + return ( +
+
+
+
+

Simulation {runId}

+ {run && ( +

+ {run.nUsers} users × {run.nRounds} rounds × {run.tasksPerRound} tasks + {' · '}{run.useLlm ? 'LLM judge' : 'Rule judge'} +

+ )} +
+ +
+ + {error &&

{error}

} + + {run && ( +
+ + {run.winner && run.status === 'done' && ( + + Winner: {run.winner} + + )} +
+ )} + + {summary && ( + <> + {/* Metric cards */} +
+ {policies.map((p) => ( +
+
{p}
+
+ + + +
+
+ ))} +
+ + {/* Cumulative reward chart */} +
+

Cumulative reward over rounds

+
+ +
+
+ + {/* Action distribution */} +
+

Action distribution

+ +
+ + )} + + {breakdown && ( +
+

Per-persona mean reward

+ +
+ )} + + {run?.status === 'running' && ( +

Simulation running — auto-refreshing every 3s…

+ )} +
+
+ ); +} + +// ── Status badge ──────────────────────────────────────────────────────────── + +function StatusBadge({ status }: { status: string }) { + const styles: Record = { + pending: 'bg-gray-800 text-gray-400', + running: 'bg-yellow-900/60 text-yellow-300 border border-yellow-700', + done: 'bg-green-900/60 text-green-300 border border-green-700', + failed: 'bg-red-900/60 text-red-300 border border-red-700', + }; + return ( + + {status} + + ); +} + +function Metric({ label, value }: { label: string; value: string | undefined }) { + return ( +
+
{label}
+
{value ?? '—'}
+
+ ); +} + +// ── Main page ─────────────────────────────────────────────────────────────── + +export default function SimulationsPage() { + const [runs, setRuns] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [selectedId, setSelectedId] = useState(null); + + // Form state + const [nUsers, setNUsers] = useState(5); + const [nRounds, setNRounds] = useState(20); + const [tasksPerRound, setTasksPerRound] = useState(8); + const [useLlm, setUseLlm] = useState(false); + const [policyA, setPolicyA] = useState('linucb-v1'); + const [policyB, setPolicyB] = useState('egreedy-v1'); + const [launching, setLaunching] = useState(false); + const [launchError, setLaunchError] = useState(''); + + const loadRuns = async () => { + setLoading(true); + try { + const { runs: r } = await getSimRuns(); + setRuns(r); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Failed to load'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { loadRuns(); }, []); + + const handleStart = async () => { + if (policyA === policyB) { + setLaunchError('Policies must be different'); + return; + } + setLaunching(true); + setLaunchError(''); + try { + const { id } = await startSimulation({ + nUsers, + nRounds, + tasksPerRound, + useLlm, + policies: [policyA, policyB], + }); + await loadRuns(); + setSelectedId(id); + } catch (e: unknown) { + setLaunchError(e instanceof Error ? e.message : 'Failed to start'); + } finally { + setLaunching(false); + } + }; + + return ( + +
+
+

Simulations

+

+ Compare recommendation policies offline using synthetic users and LLM-judged reactions. + ml/serving must be running. +

+
+ + {/* Launch form */} +
+

New simulation

+
+ + + + + + + + setNUsers(Number(e.target.value))} className={inputCls} /> + + + setNRounds(Number(e.target.value))} className={inputCls} /> + + + setTasksPerRound(Number(e.target.value))} className={inputCls} /> + + + + {!useLlm &&

Deterministic rule judge

} + {useLlm &&

Requires ANTHROPIC_API_KEY

} +
+
+ {launchError &&

{launchError}

} + +
+ + {/* Runs list */} +
+
+

Past runs

+ +
+ + {loading &&

Loading…

} + {error &&

{error}

} + + {runs.length === 0 && !loading && ( +

No simulation runs yet.

+ )} + +
+ {runs.map((run) => ( + + ))} +
+
+
+ + {selectedId && ( + setSelectedId(null)} /> + )} +
+ ); +} + +// ── Small helpers ─────────────────────────────────────────────────────────── + +const inputCls = + 'w-full bg-gray-800 border border-gray-700 rounded px-2.5 py-1.5 text-sm text-gray-200 focus:outline-none focus:border-indigo-500'; +const selectCls = + 'w-full bg-gray-800 border border-gray-700 rounded px-2.5 py-1.5 text-sm text-gray-200 focus:outline-none focus:border-indigo-500'; + +function Field({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+ + {children} +
+ ); +} diff --git a/apps/admin/src/components/AdminShell.tsx b/apps/admin/src/components/AdminShell.tsx index a24c5a9..bad8aa7 100644 --- a/apps/admin/src/components/AdminShell.tsx +++ b/apps/admin/src/components/AdminShell.tsx @@ -11,6 +11,7 @@ const NAV = [ { href: '/tips', label: 'Rec log' }, { href: '/reward-analytics', label: 'Rewards' }, { href: '/experiments', label: 'Experiments' }, + { href: '/simulations', label: 'Simulations' }, { href: '/models', label: 'Models' }, { href: '/data-quality', label: 'Data quality' }, { href: '/ops', label: 'Ops' }, diff --git a/apps/admin/src/lib/api.ts b/apps/admin/src/lib/api.ts index 58c1479..08a9721 100644 --- a/apps/admin/src/lib/api.ts +++ b/apps/admin/src/lib/api.ts @@ -220,3 +220,67 @@ export function saveQuery(name: string, querySql: string) { export function deleteSavedQuery(id: string) { return apiFetch<{ ok: boolean }>(`/admin/saved-queries/${id}`, { method: 'DELETE' }); } + +// ── Simulation ───────────────────────────────────────────────────────────── + +export interface PolicySummary { + total_reward: number; + mean_reward: number; + n_pulls: number; + cumulative_rewards: number[]; + action_counts: Record; +} + +export interface SimRun { + id: string; + policyA: string; + policyB: string; + nUsers: number; + nRounds: number; + tasksPerRound: number; + useLlm: boolean; + status: 'pending' | 'running' | 'done' | 'failed'; + summaryJson: string | null; + winner: string | null; + personaBreakdownJson: string | null; + createdAt: string; + finishedAt: string | null; + isRunning?: boolean; +} + +export interface SimEvent { + id: string; + runId: string; + round: number; + userId: string; + persona: string; + policy: string; + tipContent: string; + priority: number; + isOverdue: boolean; + action: string; + rewardMilli: number; + hour: number; + dayOfWeek: number; +} + +export function startSimulation(params: { + nUsers: number; + nRounds: number; + tasksPerRound: number; + useLlm: boolean; + policies: string[]; +}) { + return apiFetch<{ id: string; status: string }>('/admin/simulate/start', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +export function getSimRuns() { + return apiFetch<{ runs: SimRun[] }>('/admin/simulate/runs'); +} + +export function getSimRun(id: string) { + return apiFetch<{ run: SimRun; events: SimEvent[] }>(`/admin/simulate/${id}`); +} diff --git a/apps/admin/tailwind.config.ts b/apps/admin/tailwind.config.ts new file mode 100644 index 0000000..e05e8d6 --- /dev/null +++ b/apps/admin/tailwind.config.ts @@ -0,0 +1,12 @@ +import type { Config } from 'tailwindcss'; + +const config: Config = { + content: [ + './src/**/*.{ts,tsx}', + './node_modules/@tremor/**/*.{js,jsx,ts,tsx}', + ], + theme: { extend: {} }, + plugins: [], +}; + +export default config; diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json new file mode 100644 index 0000000..b8cdcc7 --- /dev/null +++ b/apps/admin/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/apps/admin/tsconfig.tsbuildinfo b/apps/admin/tsconfig.tsbuildinfo new file mode 100644 index 0000000..b9aef02 --- /dev/null +++ b/apps/admin/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.collection.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.iterator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.float16.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.error.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/global.d.ts","../../node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/index.d.ts","../../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/styled-jsx/types/css.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/styled-jsx/types/macro.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/styled-jsx/types/style.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/styled-jsx/types/global.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/styled-jsx/types/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/amp.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/amp.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/get-page-files.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/web-globals/abortcontroller.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/web-globals/domexception.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/web-globals/events.d.ts","../../node_modules/.pnpm/buffer@5.7.1/node_modules/buffer/index.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/web-globals/fetch.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/web-globals/navigator.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/web-globals/storage.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/inspector.generated.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/sea.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/sqlite.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@22.19.17/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/canary.d.ts","../../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/experimental.d.ts","../../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/index.d.ts","../../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/canary.d.ts","../../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/experimental.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/fallback.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/compiled/webpack/webpack.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/config.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/load-custom-routes.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/image-config.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/body-streams.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/cache-control.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/setup-exception-listeners.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/worker.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/constants.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/app-router-headers.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/rendering-mode.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/require-hook.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/experimental/ppr.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/plugins/app-build-manifest-plugin.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/page-types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/analysis/get-page-static-info.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/node-polyfill-crypto.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/node-environment-baseline.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/node-environment-extensions/random.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/node-environment-extensions/date.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/node-environment.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/page-extensions-type.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/instrumentation/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/coalesced-function.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/router-utils/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/constants.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/trace/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/trace/trace.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/trace/shared.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/trace/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/load-jsconfig.d.ts","../../node_modules/.pnpm/@next+env@15.5.15/node_modules/@next/env/dist/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/telemetry/storage.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/build-context.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/bloom-filter.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack-config.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-kind.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-definitions/route-definition.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/swc/generated-native.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/swc/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/dev/parse-version-info.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/next-devtools/shared/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/parse-stack.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/next-devtools/server/shared.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/next-devtools/shared/stack-frame.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/next-devtools/dev-overlay/utils/get-error-by-type.d.ts","../../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/jsx-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/next-devtools/dev-overlay/container/runtime-error/render-error.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/next-devtools/dev-overlay/shared.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/dev/hot-reloader-types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/cache-handlers/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/response-cache/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/render-result.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/i18n-provider.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/next-url.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/cookies.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/request.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/after/builtin-request-context.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/response.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/base-http/node.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/mitt.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/with-router.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/router.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/route-loader.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/page-loader.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/router/router.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/app-dir-module.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/cache-signal.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/request/fallback-params.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/response-cache/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/lazy-result.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/implicit-tags.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/deep-readonly.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/app-render.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/amp-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/error-boundary.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/layout-router.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/render-from-template-context.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/client-page.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/client-segment.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/request/search-params.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/hooks-server-context.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/types/extra-types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/types/resolvers.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/types/icons.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/metadata/metadata.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/lib/framework/boundary-components.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/rsc/preloads.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/rsc/postpone.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/rsc/taint.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/collect-segment-data.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/entry-base.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/templates/app-page.d.ts","../../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/jsx-dev-runtime.d.ts","../../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/compiler-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","../../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/client.d.ts","../../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/static.d.ts","../../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/server.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/app-page/module.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/adapter.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/use-cache/cache-life.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/flight-data-helpers.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/templates/pages.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/pages/module.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/render.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-matchers/route-matcher.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/normalizers/normalizer.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/normalizers/request/suffix.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/normalizers/request/rsc.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/normalizers/request/prefetch-rsc.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/normalizers/request/next-data.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/static-paths/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/base-server.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/async-callback-set.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","../../node_modules/.pnpm/sharp@0.34.5/node_modules/sharp/lib/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/image-optimizer.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/next-server.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/lru-cache.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/dev-bundler-service.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/dev/static-paths-worker.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/dev/next-dev-server.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/next.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/render-server.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/router-server.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/route-module.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/load-components.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/async-storage/work-store.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/http.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/redirect-status-code.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/redirect-error.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/templates/app-route.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/app-route/module.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/segment-config/app/app-segments.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/utils.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/turborepo-access-trace/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/turborepo-access-trace/result.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/turborepo-access-trace/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/export/routes/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/export/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/export/worker.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/worker.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/build/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/lib/incremental-cache/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/after/after.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/after/after-context.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/request/params.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/route-matches/route-match.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/request-meta.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/cli/next-test.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/config-shared.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/base-http/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/api-utils/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/utils.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/pages/_app.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/app.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/use-cache/cache-tag.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/cache.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/runtime-config.external.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/config.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/pages/_document.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/document.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/dynamic.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dynamic.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/pages/_error.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/error.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/head.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/head.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/request/cookies.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/request/headers.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/request/draft-mode.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/headers.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/get-img-props.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/image-component.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/shared/lib/image-external.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/image.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/link.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/link.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/redirect.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/not-found.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/forbidden.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/unauthorized.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/unstable-rethrow.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/navigation.react-server.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/unrecognized-action-error.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/components/navigation.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/navigation.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/router.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/client/script.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/script.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/web/spec-extension/image-response.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/compiled/@vercel/og/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/after/index.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/request/root-params.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/server/request/connection.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/server.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/types/global.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/types/compiled.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/types.d.ts","../../node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/index.d.ts","./next.config.ts","../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/source-map.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/previous-map.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/input.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/css-syntax-error.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/declaration.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/root.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/warning.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/lazy-result.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/no-work-result.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/processor.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/result.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/document.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/rule.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/node.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/comment.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/container.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/at-rule.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/list.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/postcss.d.ts","../../node_modules/.pnpm/postcss@8.5.9/node_modules/postcss/lib/postcss.d.mts","../../node_modules/.pnpm/tailwindcss@3.4.19_tsx@4.21.0/node_modules/tailwindcss/types/generated/corePluginList.d.ts","../../node_modules/.pnpm/tailwindcss@3.4.19_tsx@4.21.0/node_modules/tailwindcss/types/generated/colors.d.ts","../../node_modules/.pnpm/tailwindcss@3.4.19_tsx@4.21.0/node_modules/tailwindcss/types/config.d.ts","../../node_modules/.pnpm/tailwindcss@3.4.19_tsx@4.21.0/node_modules/tailwindcss/types/index.d.ts","./tailwind.config.ts","./src/middleware.ts","./src/lib/api.ts","../../node_modules/.pnpm/marked@14.1.4/node_modules/marked/lib/marked.d.ts","./src/lib/docs.ts","./src/app/layout.tsx","./src/components/AdminShell.tsx","./src/components/OverviewDashboard.tsx","./src/app/page.tsx","./src/components/AuditLog.tsx","./src/app/audit/page.tsx","./src/app/data-quality/page.tsx","./src/app/docs/page.tsx","./src/app/docs/[category]/[slug]/page.tsx","./src/app/events/page.tsx","./src/app/experiments/page.tsx","./src/app/features/page.tsx","./src/app/forbidden/page.tsx","./src/app/health/page.tsx","./src/app/login/page.tsx","./src/app/models/page.tsx","./src/app/ops/page.tsx","./src/app/reward-analytics/page.tsx","./src/app/simulations/page.tsx","./src/app/sql/page.tsx","./src/app/tips/page.tsx","./src/components/UsersTable.tsx","./src/app/users/page.tsx","./src/components/UserDetail.tsx","./src/app/users/[id]/page.tsx"],"fileIdsList":[[99,148,165,166,499],[99,148,165,166,531,534],[85,99,148,165,166,527,531],[99,148,165,166,472,482,529,531],[99,148,165,166,472,529,531],[85,99,148,165,166,531],[99,148,165,166],[99,148,165,166,531],[99,148,165,166,531,532],[99,148,165,166,531,553],[99,148,165,166,531,551],[99,148,165,166,472,482],[85,99,148,165,166,527],[85,99,148,165,166,472,527],[99,148,161,165,166,170,528],[99,148,165,166,495],[99,148,165,166,524],[99,145,146,148,165,166],[99,147,148,165,166],[148,165,166],[99,148,153,165,166,183],[99,148,149,154,159,165,166,168,180,191],[99,148,149,150,159,165,166,168],[94,95,96,99,148,165,166],[99,148,151,165,166,192],[99,148,152,153,160,165,166,169],[99,148,153,165,166,180,188],[99,148,154,156,159,165,166,168],[99,147,148,155,165,166],[99,148,156,157,165,166],[99,148,158,159,165,166],[99,147,148,159,165,166],[99,148,159,160,161,165,166,180,191],[99,148,159,160,161,165,166,175,180,183],[99,141,148,156,159,162,165,166,168,180,191],[99,148,159,160,162,163,165,166,168,180,188,191],[99,148,162,164,165,166,180,188,191],[97,98,99,100,101,102,103,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197],[99,148,159,165,166],[99,148,165,166,167,191],[99,148,156,159,165,166,168,180],[99,148,165,166,169],[99,148,165,166,170],[99,147,148,165,166,171],[99,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197],[99,148,165,166,173],[99,148,165,166,174],[99,148,159,165,166,175,176],[99,148,165,166,175,177,192,194],[99,148,160,165,166],[99,148,159,165,166,180,181,183],[99,148,165,166,182,183],[99,148,165,166,180,181],[99,148,165,166,183],[99,148,165,166,184],[99,145,148,165,166,180,185,191],[99,148,159,165,166,186,187],[99,148,165,166,186,187],[99,148,153,165,166,168,180,188],[99,148,165,166,189],[99,148,165,166,168,190],[99,148,162,165,166,174,191],[99,148,153,165,166,192],[99,148,165,166,180,193],[99,148,165,166,167,194],[99,148,165,166,195],[99,141,148,165,166],[99,141,148,159,161,165,166,171,180,183,191,193,194,196],[99,148,165,166,180,197],[85,89,99,148,165,166,199,200,201,203,443,491],[85,99,148,165,166],[85,89,99,148,165,166,199,200,201,202,358,443,491],[85,89,99,148,165,166,199,200,202,203,443,491],[85,99,148,165,166,203,358,359],[85,99,148,165,166,203,358],[85,89,99,148,165,166,200,201,202,203,443,491],[85,89,99,148,165,166,199,201,202,203,443,491],[83,84,99,148,165,166],[91,99,148,165,166],[99,148,165,166,446],[99,148,165,166,448,449,450,451],[99,148,165,166,453],[99,148,165,166,207,221,222,223,225,440],[99,148,165,166,207,246,248,250,251,254,440,442],[99,148,165,166,207,211,213,214,215,216,217,429,440,442],[99,148,165,166,440],[99,148,165,166,222,324,410,419,436],[99,148,165,166,207],[99,148,165,166,204,436],[99,148,165,166,258],[99,148,165,166,257,440,442],[99,148,162,165,166,306,324,353,497],[99,148,162,165,166,317,333,419,435],[99,148,162,165,166,371],[99,148,165,166,423],[99,148,165,166,422,423,424],[99,148,165,166,422],[93,99,148,162,165,166,204,207,211,214,218,219,220,222,226,234,235,364,389,420,440,443],[99,148,165,166,207,224,242,246,247,252,253,440,497],[99,148,165,166,224,497],[99,148,165,166,235,242,304,440,497],[99,148,165,166,497],[99,148,165,166,207,224,225,497],[99,148,165,166,249,497],[99,148,165,166,218,421,428],[99,148,165,166,174,266,436],[99,148,165,166,266,436],[85,99,148,165,166,266],[85,99,148,165,166,325],[99,148,165,166,321,369,436,479,480],[99,148,165,166,416,473,474,475,476,478],[99,148,165,166,415],[99,148,165,166,415,416],[99,148,165,166,215,365,366,367],[99,148,165,166,365,368,369],[99,148,165,166,477],[99,148,165,166,365,369],[85,99,148,165,166,208,467],[85,99,148,165,166,191],[85,99,148,165,166,224,294],[85,99,148,165,166,224],[99,148,165,166,292,296],[85,99,148,165,166,293,445],[85,89,99,148,162,165,166,198,199,200,201,202,203,443,489,490],[99,148,162,165,166],[99,148,162,165,166,211,273,365,375,390,410,425,426,440,441,497],[99,148,165,166,234,427],[99,148,165,166,443],[99,148,165,166,206],[85,99,148,165,166,306,320,332,342,344,435],[99,148,165,166,174,306,320,341,342,343,435,496],[99,148,165,166,335,336,337,338,339,340],[99,148,165,166,337],[99,148,165,166,341],[99,148,165,166,264,265,266,268],[85,99,148,165,166,259,260,261,267],[99,148,165,166,264,267],[99,148,165,166,262],[99,148,165,166,263],[85,99,148,165,166,266,293,445],[85,99,148,165,166,266,444,445],[85,99,148,165,166,266,445],[99,148,165,166,390,432],[99,148,165,166,432],[99,148,162,165,166,441,445],[99,148,165,166,329],[99,147,148,165,166,328],[99,148,165,166,236,274,312,314,316,317,318,319,362,365,435,438,441],[99,148,165,166,236,350,365,369],[99,148,165,166,317,435],[85,99,148,165,166,317,326,327,329,330,331,332,333,334,345,346,347,348,349,351,352,435,436,497],[99,148,165,166,311],[99,148,162,165,166,174,236,237,273,288,318,362,363,364,369,390,410,431,440,441,442,443,497],[99,148,165,166,435],[99,147,148,165,166,222,315,318,364,431,433,434,441],[99,148,165,166,317],[99,147,148,165,166,273,278,307,308,309,310,311,312,313,314,316,435,436],[99,148,162,165,166,278,279,307,441,442],[99,148,165,166,222,364,365,390,431,435,441],[99,148,162,165,166,440,442],[99,148,162,165,166,180,438,441,442],[99,148,162,165,166,174,191,204,211,224,236,237,239,274,275,280,285,288,314,318,365,375,377,380,382,385,386,387,388,389,410,430,431,436,438,440,441,442],[99,148,162,165,166,180],[99,148,165,166,207,208,209,211,216,219,224,242,430,438,439,443,445,497],[99,148,162,165,166,180,191,254,256,258,259,260,261,268,497],[99,148,165,166,174,191,204,246,256,284,285,286,287,314,365,380,389,390,396,399,400,410,431,436,438],[99,148,165,166,218,219,234,364,389,431,440],[99,148,162,165,166,191,208,211,314,394,438,440],[99,148,165,166,305],[99,148,162,165,166,397,398,407],[99,148,165,166,438,440],[99,148,165,166,312,315],[99,148,165,166,314,318,430,445],[99,148,162,165,166,174,240,246,287,380,390,396,399,402,438],[99,148,162,165,166,218,234,246,403],[99,148,165,166,207,239,405,430,440],[99,148,162,165,166,191,440],[99,148,162,165,166,224,238,239,240,251,269,404,406,430,440],[93,99,148,165,166,236,318,409,443,445],[99,148,162,165,166,174,191,211,218,226,234,237,274,280,284,285,286,287,288,314,365,377,390,391,393,395,410,430,431,436,437,438,445],[99,148,162,165,166,180,218,396,401,407,438],[99,148,165,166,229,230,231,232,233],[99,148,165,166,275,381],[99,148,165,166,383],[99,148,165,166,381],[99,148,165,166,383,384],[99,148,162,165,166,211,214,215,273,441],[99,148,162,165,166,174,206,208,236,274,288,318,373,374,410,438,442,443,445],[99,148,162,165,166,174,191,210,215,314,374,437,441],[99,148,165,166,307],[99,148,165,166,308],[99,148,165,166,309],[99,148,165,166,436],[99,148,165,166,255,271],[99,148,162,165,166,211,255,274],[99,148,165,166,270,271],[99,148,165,166,272],[99,148,165,166,255,256],[99,148,165,166,255,289],[99,148,165,166,255],[99,148,165,166,275,379,437],[99,148,165,166,378],[99,148,165,166,256,436,437],[99,148,165,166,376,437],[99,148,165,166,256,436],[99,148,165,166,362],[99,148,165,166,211,216,274,303,306,312,314,318,320,323,354,357,361,365,409,430,438,441],[99,148,165,166,297,300,301,302,321,322,369],[85,99,148,165,166,201,203,266,355,356],[85,99,148,165,166,201,203,266,355,356,360],[99,148,165,166,418],[99,148,165,166,222,279,317,318,329,333,365,409,411,412,413,414,416,417,420,430,435,440],[99,148,165,166,369],[99,148,165,166,373],[99,148,162,165,166,274,290,370,372,375,409,438,443,445],[99,148,165,166,297,298,299,300,301,302,321,322,369,444],[93,99,148,162,165,166,174,191,237,255,256,288,314,318,407,408,410,430,431,440,441,443],[99,148,165,166,279,281,284,431],[99,148,162,165,166,275,440],[99,148,165,166,278,317],[99,148,165,166,277],[99,148,165,166,279,280],[99,148,165,166,276,278,440],[99,148,162,165,166,210,279,281,282,283,440,441],[85,99,148,165,166,365,366,368],[99,148,165,166,241],[85,99,148,165,166,208],[85,99,148,165,166,436],[85,93,99,148,165,166,288,318,443,445],[99,148,165,166,208,467,468],[85,99,148,165,166,296],[85,99,148,165,166,174,191,206,253,291,293,295,445],[99,148,165,166,224,436,441],[99,148,165,166,392,436],[99,148,165,166,365],[85,99,148,160,162,165,166,174,206,242,248,296,443,444],[85,99,148,165,166,199,200,201,202,203,443,491],[85,86,87,88,89,99,148,165,166],[99,148,153,165,166],[99,148,165,166,243,244,245],[99,148,165,166,243],[85,89,99,148,162,164,165,166,174,198,199,200,201,202,203,204,206,237,341,402,440,442,445,491],[99,148,165,166,455],[99,148,165,166,457],[99,148,165,166,459],[99,148,165,166,461],[99,148,165,166,463,464,465],[99,148,165,166,469],[90,92,99,148,165,166,447,452,454,456,458,460,462,466,470,472,482,483,485,495,496,497,498],[99,148,165,166,471],[99,148,165,166,481],[99,148,165,166,293],[99,148,165,166,484],[99,147,148,165,166,279,281,282,284,332,436,486,487,488,491,492,493,494],[99,148,165,166,198],[99,148,165,166,516],[99,148,165,166,514,516],[99,148,165,166,505,513,514,515,517,519],[99,148,165,166,503],[99,148,165,166,506,511,516,519],[99,148,165,166,502,519],[99,148,165,166,506,507,510,511,512,519],[99,148,165,166,506,507,508,510,511,519],[99,148,165,166,503,504,505,506,507,511,512,513,515,516,517,519],[99,148,165,166,519],[99,148,165,166,501,503,504,505,506,507,508,510,511,512,513,514,515,516,517,518],[99,148,165,166,501,519],[99,148,165,166,506,508,509,511,512,519],[99,148,165,166,510,519],[99,148,165,166,511,512,516,519],[99,148,165,166,504,514],[99,148,165,166,180,198],[99,148,165,166,521,522],[99,148,165,166,520,523],[99,113,117,148,165,166,191],[99,113,148,165,166,180,191],[99,108,148,165,166],[99,110,113,148,165,166,188,191],[99,148,165,166,168,188],[99,108,148,165,166,198],[99,110,113,148,165,166,168,191],[99,105,106,109,112,148,159,165,166,180,191],[99,113,120,148,165,166],[99,105,111,148,165,166],[99,113,134,135,148,165,166],[99,109,113,148,165,166,183,191,198],[99,134,148,165,166,198],[99,107,108,148,165,166,198],[99,113,148,165,166],[99,107,108,109,110,111,112,113,114,115,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,135,136,137,138,139,140,148,165,166],[99,113,128,148,165,166],[99,113,120,121,148,165,166],[99,111,113,121,122,148,165,166],[99,112,148,165,166],[99,105,108,113,148,165,166],[99,113,117,121,122,148,165,166],[99,117,148,165,166],[99,111,113,116,148,165,166,191],[99,105,110,113,120,148,165,166],[99,148,165,166,180],[99,108,113,134,148,165,166,196,198]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"dc0a7f107690ee5cd8afc8dbf05c4df78085471ce16bdd9881642ec738bc81fe","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"0f6666b58e9276ac3a38fdc80993d19208442d6027ab885580d93aec76b4ef00","impliedFormat":1},{"version":"05fd364b8ef02fb1e174fbac8b825bdb1e5a36a016997c8e421f5fab0a6da0a0","impliedFormat":1},{"version":"631eff75b0e35d1b1b31081d55209abc43e16b49426546ab5a9b40bdd40b1f60","impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"58647d85d0f722a1ce9de50955df60a7489f0593bf1a7015521efe901c06d770","impliedFormat":1},{"version":"6b4e081d55ac24fc8a4631d5dd77fe249fa25900abd7d046abb87d90e3b45645","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6f137d651076822d4fe884287e68fd61785a0d3d1fdb250a5059b691fa897db","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"80523c00b8544a2000ae0143e4a90a00b47f99823eb7926c1e03c494216fc363","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"746911b62b329587939560deb5c036aca48aece03147b021fa680223255d5183","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"2beff543f6e9a9701df88daeee3cdd70a34b4a1c11cb4c734472195a5cb2af54","impliedFormat":1},{"version":"2e07abf27aa06353d46f4448c0bbac73431f6065eef7113128a5cd804d0c384d","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"829b9e6028b29e6a8b1c01ddb713efe59da04d857089298fa79acbdb3cfcfdef","impliedFormat":1},{"version":"24f8562308dd8ba6013120557fa7b44950b619610b2c6cb8784c79f11e3c4f90","impliedFormat":1},{"version":"5f90b8c733a1bda63e42160b15a2301051e83a6f9d5332a59d16eb12f463270d","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"ad0d1d75d129b1c80f911be438d6b61bfa8703930a8ff2be2f0e1f8a91841c64","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"496bbf339f3838c41f164238543e9fe5f1f10659cb30b68903851618464b98ba","impliedFormat":1},{"version":"5178eb4415a172c287c711dc60a619e110c3fd0b7de01ed0627e51a5336aa09c","impliedFormat":1},{"version":"ca6e5264278b53345bc1ce95f42fb0a8b733a09e3d6479c6ccfca55cdc45038c","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"fb1d8e814a3eeb5101ca13515e0548e112bd1ff3fb358ece535b93e94adf5a3a","impliedFormat":1},{"version":"ffa495b17a5ef1d0399586b590bd281056cee6ce3583e34f39926f8dcc6ecdb5","impliedFormat":1},{"version":"98b18458acb46072947aabeeeab1e410f047e0cacc972943059ca5500b0a5e95","impliedFormat":1},{"version":"361e2b13c6765d7f85bb7600b48fde782b90c7c41105b7dab1f6e7871071ba20","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"b6db56e4903e9c32e533b78ac85522de734b3d3a8541bf24d256058d464bf04b","impliedFormat":1},{"version":"24daa0366f837d22c94a5c0bad5bf1fd0f6b29e1fae92dc47c3072c3fdb2fbd5","impliedFormat":1},{"version":"570bb5a00836ffad3e4127f6adf581bfc4535737d8ff763a4d6f4cc877e60d98","impliedFormat":1},{"version":"889c00f3d32091841268f0b994beba4dceaa5df7573be12c2c829d7c5fbc232c","impliedFormat":1},{"version":"65f43099ded6073336e697512d9b80f2d4fec3182b7b2316abf712e84104db00","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"acf5a2ac47b59ca07afa9abbd2b31d001bf7448b041927befae2ea5b1951d9f9","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"d71291eff1e19d8762a908ba947e891af44749f3a2cbc5bd2ec4b72f72ea795f","impliedFormat":1},{"version":"c0480e03db4b816dff2682b347c95f2177699525c54e7e6f6aa8ded890b76be7","impliedFormat":1},{"version":"27ab780875bcbb65e09da7496f2ca36288b0c541abaa75c311450a077d54ec15","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"380647d8f3b7f852cca6d154a376dbf8ac620a2f12b936594504a8a852e71d2f","impliedFormat":1},{"version":"208c9af9429dd3c76f5927b971263174aaa4bc7621ddec63f163640cbd3c473c","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"a23185bc5ef590c287c28a91baf280367b50ae4ea40327366ad01f6f4a8edbc5","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"002eae065e6960458bda3cf695e578b0d1e2785523476f8a9170b103c709cd4f","impliedFormat":1},{"version":"c83bb0c9c5645a46c68356c2f73fdc9de339ce77f7f45a954f560c7e0b8d5ebb","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"72179f9dd22a86deaad4cc3490eb0fe69ee084d503b686985965654013f1391b","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"6a148329edecbda07c21098639ef4254ef7869fb25a69f58e5d6a8b7b69d4236","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"f63ab283a1c8f5c79fabe7ca4ef85f9633339c4f0e822fce6a767f9d59282af2","impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a54c996c8870ef1728a2c1fa9b8eaec0bf4a8001cd2583c02dd5869289465b10","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"3754982006a3b32c502cff0867ca83584f7a43b1035989ca73603f400de13c96","impliedFormat":1},{"version":"a30ae9bb8a8fa7b90f24b8a0496702063ae4fe75deb27da731ed4a03b2eb6631","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"413586add0cfe7369b64979d4ec2ed56c3f771c0667fbde1bf1f10063ede0b08","impliedFormat":1},{"version":"06472528e998d152375ad3bd8ebcb69ff4694fd8d2effaf60a9d9f25a37a097a","impliedFormat":1},{"version":"50b5bc34ce6b12eccb76214b51aadfa56572aa6cc79c2b9455cdbb3d6c76af1d","impliedFormat":1},{"version":"b7e16ef7f646a50991119b205794ebfd3a4d8f8e0f314981ebbe991639023d0e","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"a401617604fa1f6ce437b81689563dfdc377069e4c58465dbd8d16069aede0a5","impliedFormat":1},{"version":"e9dd71cf12123419c60dab867d44fbee5c358169f99529121eaef277f5c83531","impliedFormat":1},{"version":"5b6a189ba3a0befa1f5d9cb028eb9eec2af2089c32f04ff50e2411f63d70f25d","impliedFormat":1},{"version":"d6e73f8010935b7b4c7487b6fb13ea197cc610f0965b759bec03a561ccf8423a","impliedFormat":1},{"version":"174f3864e398f3f33f9a446a4f403d55a892aa55328cf6686135dfaf9e171657","impliedFormat":1},{"version":"824c76aec8d8c7e65769688cbee102238c0ef421ed6686f41b2a7d8e7e78a931","impliedFormat":1},{"version":"75b868be3463d5a8cfc0d9396f0a3d973b8c297401d00bfb008a42ab16643f13","impliedFormat":1},{"version":"15a234e5031b19c48a69ccc1607522d6e4b50f57d308ecb7fe863d44cd9f9eb3","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"4fbd3116e00ed3a6410499924b6403cc9367fdca303e34838129b328058ede40","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"6dcf60530c25194a9ee0962230e874ff29d34c59605d8e069a49928759a17e0a","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"1a42d2ec31a1fe62fdc51591768695ed4a2dc64c01be113e7ff22890bebb5e3f","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"0c7c947ff881c4274c0800deaa0086971e0bfe51f89a33bd3048eaa3792d4876","affectsGlobalScope":true,"impliedFormat":1},{"version":"db01d18853469bcb5601b9fc9826931cc84cc1a1944b33cad76fd6f1e3d8c544","affectsGlobalScope":true,"impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"15b36126e0089bfef173ab61329e8286ce74af5e809d8a72edcafd0cc049057f","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"ad10d4f0517599cdeca7755b930f148804e3e0e5b5a3847adce0f1f71bbccd74","impliedFormat":1},{"version":"1042064ece5bb47d6aba91648fbe0635c17c600ebdf567588b4ca715602f0a9d","impliedFormat":1},{"version":"c49469a5349b3cc1965710b5b0f98ed6c028686aa8450bcb3796728873eb923e","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"72d63643a657c02d3e51cd99a08b47c9b020a565c55f246907050d3c8a5e77fb","impliedFormat":1},{"version":"1d415445ea58f8033ba199703e55ff7483c52ac6742075b803bd3e7bbe9f5d61","impliedFormat":1},{"version":"d6406c629bb3efc31aedb2de809bef471e475c86c7e67f3ef9b676b5d7e0d6b2","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"71d8ba39a9e024d9e4bb922464d18542ed8d2c25ee78efa7890c27213cc6e5d3","impliedFormat":1},{"version":"8c030e515014c10a2b98f9f48408e3ba18023dfd3f56e3312c6c2f3ae1f55a16","impliedFormat":1},{"version":"dafc31e9e8751f437122eb8582b93d477e002839864410ff782504a12f2a550c","impliedFormat":1},{"version":"754498c5208ce3c5134f6eabd49b25cf5e1a042373515718953581636491f3c3","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"f56bdc6884648806d34bc66d31cdb787c4718d04105ce2cd88535db214631f82","impliedFormat":1},{"version":"633d58a237f4bb25ec7d565e4ffa32cecdcee8660ac12189c4351c52557cee9e","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"13283350547389802aa35d9f2188effaeac805499169a06ef5cd77ce2a0bd63f","impliedFormat":1},{"version":"ce791f6ea807560f08065d1af6014581eeb54a05abd73294777a281b6dfd73c2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"49f95e989b4632c6c2a578cc0078ee19a5831832d79cc59abecf5160ea71abad","impliedFormat":1},{"version":"9666533332f26e8995e4d6fe472bdeec9f15d405693723e6497bf94120c566c8","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"e17cd049a1448de4944800399daa4a64c5db8657cc9be7ef46be66e2a2cd0e7c","impliedFormat":1},{"version":"43fa6ea8714e18adc312b30450b13562949ba2f205a1972a459180fa54471018","impliedFormat":1},{"version":"6e89c2c177347d90916bad67714d0fb473f7e37fb3ce912f4ed521fe2892cd0d","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"4d4927cbee21750904af7acf940c5e3c491b4d5ebc676530211e389dd375607a","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"8a97e578a9bc40eb4f1b0ca78f476f2e9154ecbbfd5567ee72943bab37fc156a","impliedFormat":1},{"version":"c857e0aae3f5f444abd791ec81206020fbcc1223e187316677e026d1c1d6fe08","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"2d7db1d73456e8c5075387d4240c29a2a900847f9c1bff106a2e490da8fbd457","impliedFormat":1},{"version":"2b15c805f48e4e970f8ec0b1915f22d13ca6212375e8987663e2ef5f0205e832","impliedFormat":1},{"version":"f22d05663d873ee7a600faf78abb67f3f719d32266803440cf11d5db7ac0cab2","impliedFormat":1},{"version":"d93c544ad20197b3976b0716c6d5cd5994e71165985d31dcab6e1f77feb4b8f2","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"a8b1c79a833ee148251e88a2553d02ce1641d71d2921cce28e79678f3d8b96aa","impliedFormat":1},{"version":"126d4f950d2bba0bd45b3a86c76554d4126c16339e257e6d2fabf8b6bf1ce00c","impliedFormat":1},{"version":"7e0b7f91c5ab6e33f511efc640d36e6f933510b11be24f98836a20a2dc914c2d","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"2d3cc2211f352f46ea6b7cf2c751c141ffcdf514d6e7ae7ee20b7b6742da313f","impliedFormat":1},{"version":"c75445151ff8b77d9923191efed7203985b1a9e09eccf4b054e7be864e27923d","impliedFormat":1},{"version":"0aedb02516baf3e66b2c1db9fef50666d6ed257edac0f866ea32f1aa05aa474f","impliedFormat":1},{"version":"fa8a8fbf91ee2a4779496225f0312aac6635b0f21aa09cdafa4283fe32d519c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"0e8aef93d79b000deb6ec336b5645c87de167168e184e84521886f9ecc69a4b5","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"20fa37b636fdcc1746ea0738f733d0aed17890d1cd7cb1b2f37010222c23f13e","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},{"version":"19df3488557c2fc9b4d8f0bac0fd20fb59aa19dec67c81f93813951a81a867f8","affectsGlobalScope":true,"impliedFormat":1},{"version":"b25350193e103ae90423c5418ddb0ad1168dc9c393c9295ef34980b990030617","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"de7052bfee2981443498239a90c04ea5cc07065d5b9bb61b12cb6c84313ad4ef","impliedFormat":1},{"version":"a3e7d932dc9c09daa99141a8e4800fc6c58c625af0d4bbb017773dc36da75426","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"4a2edd238d9104eac35b60d727f1123de5062f452b70ed8e0366cb36387dfdfd","impliedFormat":1},{"version":"ca921bf56756cb6fe957f6af693a35251b134fb932dc13f3dfff0bb7106f80b4","impliedFormat":1},{"version":"fee92c97f1aa59eb7098a0cc34ff4df7e6b11bae71526aca84359a2575f313d8","impliedFormat":1},{"version":"0bd0297484aacea217d0b76e55452862da3c5d9e33b24430e0719d1161657225","impliedFormat":1},{"version":"2ab6d334bcbf2aff3acfc4fd8c73ecd82b981d3c3aa47b3f3b89281772286904","impliedFormat":1},{"version":"d07cbc787a997d83f7bde3877fec5fb5b12ce8c1b7047eb792996ed9726b4dde","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"4805f6161c2c8cefb8d3b8bd96a080c0fe8dbc9315f6ad2e53238f9a79e528a6","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"49179c6a23701c642bd99abe30d996919748014848b738d8e85181fc159685ff","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"45490817629431853543adcb91c0673c25af52a456479588b6486daba34f68bb","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"8514c62ce38e58457d967e9e73f128eedc1378115f712b9eef7127f7c88f82ae","impliedFormat":1},{"version":"f1289e05358c546a5b664fbb35a27738954ec2cc6eb4137350353099d154fc62","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"1d17ba45cfbe77a9c7e0df92f7d95f3eefd49ee23d1104d0548b215be56945ad","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"1d879125d1ec570bf04bc1f362fdbe0cb538315c7ac4bcfcdf0c1e9670846aa6","impliedFormat":1},{"version":"a1ee88010a64e8647d07dba58ec43e6e05851b9ec7a62e4ca2b9c33be5abb2c8","impliedFormat":1},{"version":"46273e8c29816125d0d0b56ce9a849cc77f60f9a5ba627447501d214466f0ff3","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"e91f7b1344577a02f051b9b471f33044fef8334a76dc9e1de003d17595a5219b","impliedFormat":1},{"version":"3af3584f79c57853028ef9421ec172539e1fe01853296dc05a9d615ade4ffaf6","impliedFormat":1},{"version":"f82579d87701d639ff4e3930a9b24f4ee13ca74221a9a3a792feb47f01881a9c","impliedFormat":1},{"version":"d7e5d5245a8ba34a274717d085174b2c9827722778129b0081fefd341cca8f55","impliedFormat":1},{"version":"d9d32f94056181c31f553b32ce41d0ef75004912e27450738d57efcd2409c324","impliedFormat":1},{"version":"752513f35f6cff294ffe02d6027c41373adf7bfa35e593dbfd53d95c203635ee","impliedFormat":1},{"version":"6c800b281b9e89e69165fd11536195488de3ff53004e55905e6c0059a2d8591e","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"1a7e2ea171726446850ec72f4d1525d547ff7e86724cc9e7eec509725752a758","impliedFormat":1},{"version":"8c901126d73f09ecdea4785e9a187d1ac4e793e07da308009db04a7283ec2f37","impliedFormat":1},{"version":"db97922b767bd2675fdfa71e08b49c38b7d2c847a1cc4a7274cb77be23b026f1","impliedFormat":1},{"version":"aab290b8e4b7c399f2c09b957666fc95335eb4522b2dd9ead1bf0cb64da6d6ee","impliedFormat":1},{"version":"94fe3281392e1015b22f39535878610b4fa6f1388dc8d78746be3bc4e4bb8950","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"06c25ddfc2242bd06c19f66c9eae4c46d937349a267810f89783680a1d7b5259","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"bd4131091b773973ca5d2326c60b789ab1f5e02d8843b3587effe6e1ea7c9d86","impliedFormat":1},{"version":"c7f6485931085bf010fbaf46880a9b9ec1a285ad9dc8c695a9e936f5a48f34b4","impliedFormat":1},{"version":"14f6b927888a1112d662877a5966b05ac1bf7ed25d6c84386db4c23c95a5363b","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"0427df5c06fafc5fe126d14b9becd24160a288deff40e838bfbd92a35f8d0d00","impliedFormat":1},{"version":"90c54a02432d04e4246c87736e53a6a83084357acfeeba7a489c5422b22f5c7a","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"83fe880c090afe485a5c02262c0b7cdd76a299a50c48d9bde02be8e908fb4ae6","impliedFormat":1},{"version":"0a372c2d12a259da78e21b25974d2878502f14d89c6d16b97bd9c5017ab1bc12","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"6511e4503cf74c469c60aafd6589e4d14d5eb0a25f9bf043dcbecdf65f261972","impliedFormat":1},{"version":"ec1ca97598eda26b7a5e6c8053623acbd88e43be7c4d29c77ccd57abc4c43999","impliedFormat":1},{"version":"6e2261cd9836b2c25eecb13940d92c024ebed7f8efe23c4b084145cd3a13b8a6","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"a47e6d954d22dd9ebb802e7e431b560ed7c581e79fb885e44dc92ed4f60d4c07","impliedFormat":1},{"version":"f019e57d2491c159d47a107fd90219a1734bdd2e25cd8d1db3c8fae5c6b414c4","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"d1c9bf292a54312888a77bb19dba5e2503ad803f5393beafd45d78d2f4fe9b48","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"cb8d8ef7b9ce8ed3e6f1c814fcbf3f90dab0cb8863079236784fc350746e27c4","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"3be035da7bee86b4c3abf392e0edaa44fc6e45092995eefe36b39118c8a84068","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f828825d077c2fa0ea606649faeb122749273a353daab23924fe674e98ba44c","impliedFormat":1},{"version":"2896c2e673a5d3bd9b4246811f79486a073cbb03950c3d252fba10003c57411a","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"407a06ba04eede4074eec470ecba2784cbb3bf4e7de56833b097dd90a2aa0651","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"5c96bad5f78466785cdad664c056e9e2802d5482ca5f862ed19ba34ffbb7b3a4","impliedFormat":1},{"version":"81d8603ac527e75cfec72bb9391228b58f161c2b33514a9d814c7f3ebd3ef466","impliedFormat":1},{"version":"5f3dc10ae646f375776b4e028d2bed039a93eebbba105694d8b910feebbe8b9c","impliedFormat":1},{"version":"bb0cd7862b72f5eba39909c9889d566e198fcaddf7207c16737d0c2246112678","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"bad68fd0401eb90fe7da408565c8aee9c7a7021c2577aec92fa1382e8876071a","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"fec01479923e169fb52bd4f668dbeef1d7a7ea6e6d491e15617b46f2cacfa37d","impliedFormat":1},{"version":"8a8fb3097ba52f0ae6530ec6ab34e43e316506eb1d9aa29420a4b1e92a81442d","impliedFormat":1},{"version":"44e09c831fefb6fe59b8e65ad8f68a7ecc0e708d152cfcbe7ba6d6080c31c61e","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"4655709c9cb3fd6db2b866cab7c418c40ed9533ce8ea4b66b5f17ec2feea46a9","impliedFormat":1},{"version":"87affad8e2243635d3a191fa72ef896842748d812e973b7510a55c6200b3c2a4","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"3eecb25bb467a948c04874d70452b14ae7edb707660aac17dc053e42f2088b00","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"330896c1a2b9693edd617be24fbf9e5895d6e18c7955d6c08f028f272b37314d","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"5f0292a40df210ab94b9fb44c8b775c51e96777e14e073900e392b295ca1061b","impliedFormat":1},{"version":"bc9ee0192f056b3d5527bcd78dc3f9e527a9ba2bdc0a2c296fbc9027147df4b2","impliedFormat":1},{"version":"8627ad129bcf56e82adff0ab5951627c993937aa99f5949c33240d690088b803","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"a68d4b3182e8d776cdede7ac9630c209a7bfbb59191f99a52479151816ef9f9e","impliedFormat":99},{"version":"39644b343e4e3d748344af8182111e3bbc594930fff0170256567e13bbdbebb0","impliedFormat":99},{"version":"ed7fd5160b47b0de3b1571c5c5578e8e7e3314e33ae0b8ea85a895774ee64749","impliedFormat":99},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"ecbaf0da125974be39c0aac869e403f72f033a4e7fd0d8cd821a8349b4159628","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"ceec3c81b2d81f5e3b855d9367c1d4c664ab5046dff8fd56552df015b7ccbe8f","affectsGlobalScope":true,"impliedFormat":1},{"version":"8fac4a15690b27612d8474fb2fc7cc00388df52d169791b78d1a3645d60b4c8b","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"1d63055b690a582006435ddd3aa9c03aac16a696fac77ce2ed808f3e5a06efab","impliedFormat":1},{"version":"8e83dfe51166d7ccb462bd3df39caa83f2a4eb9c543b64c4cd53d002b241ddc4","signature":"fe30465f81a37f23a168022109187dbd1951404e3ec2fbc6516d584c597f1325"},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"00cf4001e0d9c6e5e036bc545b9d73e2b8b84cddb02e61ad05bab3752b1d4522","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"b558c9a18ea4e6e4157124465c3ef1063e64640da139e67be5edb22f534f2f08","impliedFormat":1},{"version":"01374379f82be05d25c08d2f30779fa4a4c41895a18b93b33f14aeef51768692","impliedFormat":1},{"version":"b0dee183d4e65cf938242efaf3d833c6b645afb35039d058496965014f158141","impliedFormat":1},{"version":"c0bbbf84d3fbd85dd60d040c81e8964cc00e38124a52e9c5dcdedf45fea3f213","impliedFormat":1},{"version":"79b29d17a3545646ecd76c863c1ded92aab47066a9e7ec62498c6b51bdd25db5","signature":"f2542ed28646ccec19a2b407da97ef71777f4a2722da6990c958c2c9612ae978"},{"version":"98a7f18549cffb00796ed742ff4ddf111e2ebff251734a2aa5479cc5141ea527","signature":"d80c4149ceee3def8a8d66e291c51ed57b6021239bb07a44c352539628e7791c"},{"version":"9bebc96b22e60ec7e2c8551db88c9a40c4b0cab5fa0945196ef4368163075876","signature":"14e8417244b9accf4aa0599a0c3b247eb62c5e69a20e0d5ee320695a265b9a0f"},{"version":"bef0f735f5cf6fbd3e4dd1ebde84aa5b0f4cf87faee1a49b67006bf88b24555d","impliedFormat":99},{"version":"95ca982c8c63a4941cbb2dbd03a0609f9544a13323ca3163a2ce4d8fc799694c","signature":"14ed43e2f565f096e3d6b429e78885bd92ef1990c163997e776519cef055a7d4"},{"version":"2dd494f0ea9072a7415659b921d34570885def0cfeabd71ec4e4355d03832658","signature":"2dcc50c46dcccaadb05bc414e290fd1a75298fcadb47c77c1af84945ec8b6b01"},{"version":"b3fad71d71a074e02fd208c65c4bc38f38633e2fceac88da744f7a10e5177a76","signature":"27f137787ead6308f0a2e1cb7283eb7d2185dbc24e9e44b7f26c169637bb5638"},"5c3d43fd2fef6747f4f2ac013c08b2e9aedb7b7de86e14db4fcd7843fe7fb738","b6f3565dcb19ae5c8f54ca3e9a3150e33a2c9d5ce489f1819d1d5cbff2599738","bdeb8be7846489a834374ee82d983f77d8327ac034cacc4398fac9b31cf60868","3b550137edd669f806bba097e652faf19cbb0d5e15169cd1a92bdd0257e4e5ae","c03506b930410afda3608c9e13fd687f3760a4d4ffa8def463f9ec93f8de6a70",{"version":"536e692fad1ccefb0493a43da94ba0a94ead5d47f37beaa7254f224169ffbba7","signature":"9d091d5cec7aa810f43a111f2b39b7f368aaf20e075fcf849709abef61503396"},{"version":"f808959991e263533a769e7df64ba1ddb375bdcf8698f43d9afa90e531e1e1c5","signature":"f7051a57c956e9baa36050ad20416ab89101e1801d8804a5867af89fd78a6fb9"},"75974177e90be1a31c9f7e9d4c771e0ca792a585519a00d7359f33b6b5a77d25","7148e8d563d2e2ab6a1fe031e1b9bd1675251e093fba37f0526d1170c69b5779",{"version":"d04149a193cf80b34d04d7b183a5fe8cff32ec435af33110acaa6c6ba5ca5294","signature":"1e557b5c8899c5942ab023e47b22ea1e655074b4f32c84ecbde02343e7d8187d"},{"version":"224a964c4511c03779f0f623c06987cbb4a5722181ea4e4e92c93507fd4854bf","signature":"7ea26840bca67fb07966011a93a9a063a1535cfee2b01bf4d4d712ef9e0aa36a"},"34e059c849e70caa75f850d541be52dca3d2735858965a75100ccdddb42c40a2",{"version":"996e6f79fde8899aab4e5579729a2092fdcf1e8b457ef935f6ae5f476e13e192","signature":"45b373ad2e114de335dd3eaf62f9658266d71c2f34537489f88f3b4815fa72f8"},{"version":"aeed02b0749d027ce510d925f8a6b539a0a2766f8ac95d0cbb1678a82ab9fad1","signature":"bd3015b381cadb30e9e77d2608e64d909efd260a900654e6418f2507d7aac081"},"5743311bae16373e10cbcd5789e827fc7a88d74d4b1550ec912db1664800563e","37342dd7ea8413d97c87090d7d827d711c712462c6618665d84b0ccc41aa8522",{"version":"c383254f4830353524b0a3493c282f0d41c21a674a3f419d65affdbc9eed878b","signature":"a55f8da3afd0d24b7a514775654bbfa8e382dea328a6b879c08a735bf1892023"},"5610540080439443139157d4f3be5d920d993d891443e5adc064a3f51f06d869","07d8ab138892fc1493f0297533d4d10b4c9c6845df93d0f627030f83a564532a","0c920195d5ee4c90df3f8d3c4c1f501f0b7fd47e185283aaa61d1deb79f5efb3","af4bb300163ee70d5b074eea46379e1bedf3766583f6bb6fccfc2ff88baaf83f","9cd4520dfba2915b9b9d0c282ed4afce7a872a973eeaa0f51a3711071fbe2374","52b77075ec3d7516e54eaae58639b851f50796f8eb863f33f5f9d122924369d0"],"root":[500,[525,527],[529,554]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"strict":true,"target":4},"referencedMap":[[500,1],[535,2],[536,3],[538,4],[537,5],[539,3],[540,3],[541,6],[542,7],[543,3],[530,1],[544,7],[545,8],[546,3],[533,9],[547,3],[548,3],[549,3],[550,3],[554,10],[552,11],[531,12],[534,13],[532,13],[553,13],[551,14],[527,7],[529,15],[526,16],[525,17],[248,7],[145,18],[146,18],[147,19],[99,20],[148,21],[149,22],[150,23],[94,7],[97,24],[95,7],[96,7],[151,25],[152,26],[153,27],[154,28],[155,29],[156,30],[157,30],[158,31],[159,32],[160,33],[161,34],[100,7],[98,7],[162,35],[163,36],[164,37],[198,38],[165,39],[166,7],[167,40],[168,41],[169,42],[170,43],[171,44],[172,45],[173,46],[174,47],[175,48],[176,48],[177,49],[178,7],[179,50],[180,51],[182,52],[181,53],[183,54],[184,55],[185,56],[186,57],[187,58],[188,59],[189,60],[190,61],[191,62],[192,63],[193,64],[194,65],[195,66],[101,7],[102,7],[103,7],[142,67],[143,7],[144,7],[196,68],[197,69],[202,70],[358,71],[203,72],[201,73],[360,74],[359,75],[199,76],[356,7],[200,77],[83,7],[85,78],[355,71],[266,71],[104,7],[84,7],[528,7],[92,79],[447,80],[452,81],[454,82],[224,83],[252,84],[430,85],[247,86],[235,7],[216,7],[222,7],[420,87],[283,88],[223,7],[389,89],[257,90],[258,91],[354,92],[417,93],[372,94],[424,95],[425,96],[423,97],[422,7],[421,98],[254,99],[225,100],[304,7],[305,101],[220,7],[236,102],[226,103],[288,102],[285,102],[209,102],[250,104],[249,7],[429,105],[439,7],[215,7],[330,106],[331,107],[325,71],[475,7],[333,7],[334,108],[326,109],[481,110],[479,111],[474,7],[416,112],[415,7],[473,113],[327,71],[368,114],[366,115],[476,7],[480,7],[478,116],[477,7],[367,117],[468,118],[471,119],[295,120],[294,121],[293,122],[484,71],[292,123],[277,7],[487,7],[490,7],[489,71],[491,124],[205,7],[426,125],[427,126],[428,127],[238,7],[214,128],[204,7],[346,71],[207,129],[345,130],[344,131],[335,7],[336,7],[343,7],[338,7],[341,132],[337,7],[339,133],[342,134],[340,133],[221,7],[212,7],[213,102],[267,135],[268,136],[265,137],[263,138],[264,139],[260,7],[352,108],[374,108],[446,140],[455,141],[459,142],[433,143],[432,7],[280,7],[492,144],[442,145],[328,146],[329,147],[320,148],[310,7],[351,149],[311,150],[353,151],[348,152],[347,7],[349,7],[365,153],[434,154],[435,155],[313,156],[317,157],[308,158],[412,159],[441,160],[287,161],[390,162],[210,163],[440,164],[206,86],[261,7],[269,165],[401,166],[259,7],[400,167],[93,7],[395,168],[237,7],[306,169],[391,7],[211,7],[270,7],[399,170],[219,7],[275,171],[316,172],[431,173],[315,7],[398,7],[262,7],[403,174],[404,175],[217,7],[406,176],[408,177],[407,178],[240,7],[397,163],[410,179],[396,180],[402,181],[228,7],[231,7],[229,7],[233,7],[230,7],[232,7],[234,182],[227,7],[382,183],[381,7],[387,184],[383,185],[386,186],[385,186],[388,184],[384,185],[274,187],[375,188],[438,189],[494,7],[463,190],[465,191],[312,7],[464,192],[436,154],[493,193],[332,154],[218,7],[314,194],[271,195],[272,196],[273,197],[303,198],[411,198],[289,198],[376,199],[290,199],[256,200],[255,7],[380,201],[379,202],[378,203],[377,204],[437,205],[324,206],[362,207],[323,208],[357,209],[361,210],[419,211],[418,212],[414,213],[371,214],[373,215],[370,216],[409,217],[364,7],[451,7],[363,218],[413,7],[276,219],[309,125],[307,220],[278,221],[281,222],[488,7],[279,223],[282,223],[449,7],[448,7],[450,7],[486,7],[284,224],[322,71],[91,7],[369,225],[253,7],[242,226],[318,7],[457,71],[467,227],[302,71],[461,108],[301,228],[444,229],[300,227],[208,7],[469,230],[298,71],[299,71],[291,7],[241,7],[297,231],[296,232],[239,233],[319,47],[286,47],[405,7],[393,234],[392,7],[453,7],[350,235],[321,71],[445,236],[86,71],[89,237],[90,238],[87,71],[88,7],[251,239],[246,240],[245,7],[244,241],[243,7],[443,242],[456,243],[458,244],[460,245],[462,246],[466,247],[470,248],[499,249],[472,250],[482,251],[483,252],[485,253],[495,254],[498,128],[497,7],[496,255],[517,256],[515,257],[516,258],[504,259],[505,257],[512,260],[503,261],[508,262],[518,7],[509,263],[514,264],[520,265],[519,266],[502,267],[510,268],[511,269],[506,270],[513,256],[507,271],[394,272],[501,7],[523,273],[522,7],[521,7],[524,274],[81,7],[82,7],[13,7],[14,7],[16,7],[15,7],[2,7],[17,7],[18,7],[19,7],[20,7],[21,7],[22,7],[23,7],[24,7],[3,7],[25,7],[26,7],[4,7],[27,7],[31,7],[28,7],[29,7],[30,7],[32,7],[33,7],[34,7],[5,7],[35,7],[36,7],[37,7],[38,7],[6,7],[42,7],[39,7],[40,7],[41,7],[43,7],[7,7],[44,7],[49,7],[50,7],[45,7],[46,7],[47,7],[48,7],[8,7],[54,7],[51,7],[52,7],[53,7],[55,7],[9,7],[56,7],[57,7],[58,7],[60,7],[59,7],[61,7],[62,7],[10,7],[63,7],[64,7],[65,7],[11,7],[66,7],[67,7],[68,7],[69,7],[70,7],[1,7],[71,7],[72,7],[12,7],[76,7],[74,7],[79,7],[78,7],[73,7],[77,7],[75,7],[80,7],[120,275],[130,276],[119,275],[140,277],[111,278],[110,279],[139,255],[133,280],[138,281],[113,282],[127,283],[112,284],[136,285],[108,286],[107,255],[137,287],[109,288],[114,289],[115,7],[118,289],[105,7],[141,290],[131,291],[122,292],[123,293],[125,294],[121,295],[124,296],[134,255],[116,297],[117,298],[126,299],[106,300],[129,291],[128,289],[132,7],[135,301]],"affectedFilesPendingEmit":[500,535,536,538,537,539,540,541,542,543,530,544,545,546,533,547,548,549,550,554,552,531,534,532,553,551,527,529,526,525],"version":"5.9.3"} \ No newline at end of file diff --git a/apps/web/e2e/sign-in.spec.ts b/apps/web/e2e/sign-in.spec.ts new file mode 100644 index 0000000..c94dd03 --- /dev/null +++ b/apps/web/e2e/sign-in.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from '@playwright/test'; + +test('sign-in page loads and shows Google button', async ({ page }) => { + await page.goto('/sign-in'); + await expect(page.getByRole('link', { name: /google/i })).toBeVisible(); +}); + +test('unauthenticated root redirects to sign-in', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveURL(/sign-in/); +}); diff --git a/apps/web/package.json b/apps/web/package.json index 44050db..8ee65df 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,6 +7,10 @@ "build": "next build", "start": "next start -p 3079", "lint": "next lint", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", "type-check": "tsc --noEmit", "clean": "rm -rf .next" }, @@ -17,9 +21,17 @@ "react-dom": "^19.0.0" }, "devDependencies": { + "@playwright/test": "^1.59.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^22.10.5", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", - "@types/node": "^22.10.5", - "typescript": "^5.7.3" + "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "^4.1.4", + "jsdom": "^29.0.2", + "typescript": "^5.7.3", + "vitest": "^4.1.4" } } diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts new file mode 100644 index 0000000..46c6c23 --- /dev/null +++ b/apps/web/playwright.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: 'html', + use: { + baseURL: process.env.BASE_URL ?? 'http://localhost:3079', + trace: 'on-first-retry', + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + ], + // Start dev server automatically in CI; locally, run `pnpm dev` first + webServer: process.env.CI + ? { + command: 'pnpm build && pnpm start', + url: 'http://localhost:3079', + reuseExistingServer: false, + } + : undefined, +}); diff --git a/apps/web/src/components/__tests__/TipPage.test.tsx b/apps/web/src/components/__tests__/TipPage.test.tsx new file mode 100644 index 0000000..a79dddb --- /dev/null +++ b/apps/web/src/components/__tests__/TipPage.test.tsx @@ -0,0 +1,131 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor, act, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +// Mock the API module — we test UI behaviour, not network calls +vi.mock('@/lib/api', () => ({ + getRecommendation: vi.fn(), + sendFeedback: vi.fn().mockResolvedValue(undefined), + getVapidPublicKey: vi.fn(), + subscribePush: vi.fn(), +})); + +import { getRecommendation, sendFeedback } from '@/lib/api'; +import TipPage from '@/app/tip/page'; + +const mockGetRec = getRecommendation as ReturnType; +const mockSendFeedback = sendFeedback as ReturnType; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('TipPage — empty / error states', () => { + it('shows "All clear." when no tip is returned', async () => { + mockGetRec.mockResolvedValue(null); + render(); + await waitFor(() => expect(screen.getByText('All clear.')).toBeInTheDocument()); + }); + + it('shows "All clear." when getRecommendation throws', async () => { + mockGetRec.mockRejectedValue(Object.assign(new Error('Network error'), { status: 503 })); + render(); + await waitFor(() => expect(screen.getByText('All clear.')).toBeInTheDocument()); + }); + + it('"Check again" button re-calls getRecommendation', async () => { + mockGetRec.mockResolvedValue(null); + render(); + await waitFor(() => screen.getByText('Check again')); + + mockGetRec.mockResolvedValue({ + tip: { id: 'todoist:2', content: 'New tip', source: 'todoist', createdAt: '' }, + }); + fireEvent.click(screen.getByText('Check again')); + await waitFor(() => expect(mockGetRec).toHaveBeenCalledTimes(2)); + }); +}); + +describe('TipPage — tip display', () => { + it('renders tip content after loading', async () => { + mockGetRec.mockResolvedValue({ + tip: { id: 'todoist:1', content: 'Write the test', source: 'todoist', createdAt: '' }, + }); + render(); + await waitFor(() => expect(screen.getByText('Write the test')).toBeInTheDocument()); + }); + + it('shows "hold to act" hint when tip is displayed', async () => { + mockGetRec.mockResolvedValue({ + tip: { id: 'todoist:3', content: 'Do the thing', source: 'todoist', createdAt: '' }, + }); + render(); + await waitFor(() => expect(screen.getByText(/hold to act/i)).toBeInTheDocument()); + }); + + it('shows "reading you…" while loading', async () => { + // Never resolves during this assertion + mockGetRec.mockReturnValue(new Promise(() => {})); + render(); + expect(screen.getByText(/reading you/i)).toBeInTheDocument(); + }); +}); + +describe('TipPage — action sheet', () => { + // Render with real timers, THEN switch to fake for hold simulation + async function renderTipAndHold(id: string, content: string) { + mockGetRec.mockResolvedValue({ tip: { id, content, source: 'todoist', createdAt: '' } }); + render(); + // Wait for tip to appear (real timers — no deadlock) + await screen.findByText(content); + const main = screen.getByRole('main'); + + // Switch to fake timers now that the component is fully loaded + vi.useFakeTimers(); + act(() => { main.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); }); + act(() => { vi.advanceTimersByTime(650); }); + vi.useRealTimers(); + + // Wait for action sheet + await screen.findByText('Done ✓'); + return main; + } + + it('action sheet appears after a long press (600 ms)', async () => { + await renderTipAndHold('tip:lp', 'Hold me'); + expect(screen.getByText('Done ✓')).toBeInTheDocument(); + }); + + it('action sheet does not appear on short press (<600 ms)', async () => { + mockGetRec.mockResolvedValue({ tip: { id: 'tip:sp', content: 'Short press', source: 'todoist', createdAt: '' } }); + render(); + await screen.findByText('Short press'); + const main = screen.getByRole('main'); + + vi.useFakeTimers(); + act(() => { main.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); }); + act(() => { vi.advanceTimersByTime(200); }); + act(() => { main.dispatchEvent(new PointerEvent('pointerup', { bubbles: true })); }); + vi.useRealTimers(); + + expect(screen.queryByText('Done ✓')).not.toBeInTheDocument(); + }); + + it('clicking "Done ✓" calls sendFeedback with action=done', async () => { + await renderTipAndHold('tip:d', 'Do it'); + await act(async () => { fireEvent.click(screen.getByText('Done ✓')); }); + expect(mockSendFeedback).toHaveBeenCalledWith('tip:d', { action: 'done' }); + }); + + it('clicking "Dismiss" calls sendFeedback with action=dismiss', async () => { + await renderTipAndHold('tip:dis', 'Dismiss me'); + await act(async () => { fireEvent.click(screen.getByText('Dismiss')); }); + expect(mockSendFeedback).toHaveBeenCalledWith('tip:dis', { action: 'dismiss' }); + }); + + it('clicking "Helpful" calls sendFeedback with action=helpful (non-navigating)', async () => { + await renderTipAndHold('tip:help', 'Helpful tip'); + await act(async () => { fireEvent.click(screen.getByText('Helpful')); }); + expect(mockSendFeedback).toHaveBeenCalledWith('tip:help', { action: 'helpful' }); + }); +}); diff --git a/apps/web/src/test/setup.ts b/apps/web/src/test/setup.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/apps/web/src/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts new file mode 100644 index 0000000..fda80bf --- /dev/null +++ b/apps/web/vitest.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + exclude: ['e2e/**', 'node_modules/**'], + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['src/**'], + }, + }, + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, +}); diff --git a/docs/adr/0006-admin-console-framework.md b/docs/adr/0006-admin-console-framework.md new file mode 100644 index 0000000..8fe3112 --- /dev/null +++ b/docs/adr/0006-admin-console-framework.md @@ -0,0 +1,59 @@ +# ADR-0006: Admin console framework — Next.js 15 + Tremor + shadcn/ui + embed specialist tools + +## Status +Accepted — 2026-04-15 + +## Context +M1 ships a bandit-driven recommender, an event bus, and a live feedback loop. Without a cockpit to observe these systems, every model change ships blind. An admin console is needed to: + +1. **Observe** — DAU/WAU, tip outcomes, reaction rates, LinUCB arm stats, feature distributions +2. **Inspect** — per-user identity, consents, integrations, reward history +3. **Act** — revoke tokens, replay signals, reset a per-user bandit, promote a policy +4. **Audit** — every operator action is logged + +The team is two people. The stack is TypeScript/React/Tailwind. Any framework that forks the stack creates a context-switch tax and a second deployment surface. + +## Decision + +### App shell — `apps/admin`, Next.js 15, App Router + +Same stack as `apps/web`. Reuses `packages/shared-types`, the Auth.js session cookie, and the API rewrite convention. Deployed at `admin.o.alogins.net` behind Caddy, port 3080 in dev. + +### UI libraries + +| Layer | Library | Reason | +|-------|---------|--------| +| Charts / KPI | **Tremor** | Analytics-first React + Tailwind components (KPI cards, time-series, bar lists). Designed for dashboards, not bolted on. | +| CRUD primitives | **shadcn/ui** | Copy-paste Radix components; forms, dialogs, command palette. No version lock-in — code lives in-repo. | +| Heavy grids | **TanStack Table v8** | Sortable / paginated / virtualized tables for events, users, tips. | +| Extra charts | **Recharts** | Fallback where Tremor falls short (histograms, distributions). | + +### Embed, don't rebuild + +Specialized tooling is **reverse-proxied into the admin shell**, not reimplemented: + +- **MLflow UI** → `/admin/models` (Caddy sub-path proxy) +- **Grafana panels** → `/admin/infra` (iframed or embedded panels) +- **Marimo notebooks** → launch-out link from admin + +This prevents reimplementing artifact browsers or graph renderers we'd never do as well. + +### AuthZ + +`profile.role` column on the `users` table (values: `'user'` | `'admin'`). First admin seeded via `ADMIN_SEED_EMAIL` env var at startup. Admin-only gate in Next.js middleware checks the session and the role returned by `GET /api/user/me`. Every write action through the admin API is appended to an `admin_actions` audit log. + +### Rejected alternatives + +| Option | Rejected because | +|--------|-----------------| +| Retool / AppSmith | Admin logic leaves the repo; weak analytics affordances | +| Streamlit / Gradio | Python-first; splits the frontend stack; thin RBAC | +| React-admin / Refine.dev | Strong CRUD scaffolding, analytics views feel bolted on | +| Superset / Metabase as the admin surface | Excellent BI, poor operational writes; plan: adopt Superset in M4 for BI alongside batch pipelines | + +## Consequences + +- One more Next.js app in the monorepo. Build/dev added to Turborepo. +- Tremor + shadcn/ui are added as dependencies. shadcn components are copied into `apps/admin/src/components/ui/` — no runtime version coupling. +- MLflow and Grafana must be reachable from the Caddy reverse proxy; they are not embedded in the JS bundle. +- `admin_actions` audit log grows unboundedly — needs a retention policy before M4. diff --git a/docs/adr/0007-egreedy-v1-active-policy.md b/docs/adr/0007-egreedy-v1-active-policy.md new file mode 100644 index 0000000..fbe506b --- /dev/null +++ b/docs/adr/0007-egreedy-v1-active-policy.md @@ -0,0 +1,47 @@ +# ADR-0007: ε-greedy v1 as the active recommendation policy + +## Status +Accepted — 2026-04-16 + +## Context + +M1 shipped LinUCB (d=5, α=1.0) as the first learned policy via `ml/serving /score`. After the M1 admin console landed, we ran an offline simulation to compare LinUCB against a new ε-greedy ridge-regression policy before deciding which to keep live. + +**ε-greedy v1 design:** +- Ridge regression estimator, θ updated online (equivalent to LinUCB without the UCB bonus). +- d=7 feature vector: base 5 (is\_overdue, task\_age\_days, priority, hour\_of\_day, bias) + sin/cos encoding of day\_of\_week. +- ε=0.10 random exploration; 90% argmax(θ·x). +- Separate per-user state files (`{user}_egreedy.json`), independent of LinUCB state. + +**Simulation setup (rule judge, seed=42):** +- 5 synthetic personas × 20 rounds × 8 tasks/round = 100 judgments per policy. +- Reward inferred from dwell-time (same `inferReward` logic as production): dismiss=−1, snooze=+0.1, done<15 s=−0.3, done 15 s–2 min=+1.0, done 2–10 min=+0.6, done>10 min=+0.3. +- Both policies started from blank state (no warm-up). + +**Results:** + +| Policy | Total reward | Mean reward/pull | Pulls | +|--------|-------------|-----------------|-------| +| egreedy-v1 | −54.80 | −0.548 | 100 | +| linucb-v1 | −60.60 | −0.606 | 100 | + +Winner: **egreedy-v1** (+10.7% mean reward). + +Both policies produce negative mean rewards under the dwell-time model — expected: most simulated users don't act in the 15s–2min magic zone on cold models. The gap widens from round 8 onward, consistent with LinUCB's UCB exploration bonus over-favouring high-uncertainty dimensions (is\_overdue, task\_age\_days) regardless of persona fit. + +## Decision + +Promote **egreedy-v1** to the active serving policy: +- `POST /recommend` calls `/score/egreedy` instead of `/score`. +- Feedback loop calls `/reward/egreedy`. +- LinUCB (`/score`, `/reward`) remains deployed in `ml/serving` as a shadow-eligible fallback. + +The simulation does not replace online A/B testing; it is evidence that egreedy-v1 is worth promoting before collecting real-user signal. A future milestone will run live A/B once we have enough daily active users for statistical power. + +## Consequences + +- Recommendation calls and reward updates now hit the egreedy endpoints only. +- LinUCB state is preserved on disk; re-activation is a one-line change. +- `tip_scores.policy` will log `egreedy-v1` for new serves; historical rows remain `linucb-v1` or `random`. +- The dwell-time reward model (`inferReward`) is now the canonical feedback signal for both online updates and simulation. Explicit helpful/not\_helpful signals are removed. +- Next evaluation gate: once ≥500 real tips served with egreedy-v1, compare reward distribution to the LinUCB historical baseline in the admin Reward Analytics page before deciding on next policy iteration. diff --git a/infra/ci/ci.yml b/infra/ci/ci.yml index 9b55f27..b3bb93f 100644 --- a/infra/ci/ci.yml +++ b/infra/ci/ci.yml @@ -23,6 +23,22 @@ jobs: - run: pnpm build --filter=@oo/shared-types - run: pnpm type-check + test: + name: Unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build --filter=@oo/shared-types + - run: pnpm test + ml-lint: name: Python lint runs-on: ubuntu-latest @@ -33,3 +49,15 @@ jobs: python-version: '3.12' - run: pip install ruff - run: ruff check ml/serving/ + + ml-test: + name: Python tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m venv ml/serving/.venv + - run: ml/serving/.venv/bin/pip install -r ml/serving/requirements-dev.txt + - run: ml/serving/.venv/bin/python -m pytest ml/serving/tests/ -v diff --git a/ml/experiments/sim/llm_judge.py b/ml/experiments/sim/llm_judge.py new file mode 100644 index 0000000..01402fc --- /dev/null +++ b/ml/experiments/sim/llm_judge.py @@ -0,0 +1,204 @@ +""" +LLM-based user reaction judge. + +Uses Claude Haiku when ANTHROPIC_API_KEY is set; falls back to a +deterministic persona-based rule when it is not. +""" + +from __future__ import annotations + +import os +import random + +from personas import Persona + +ACTIONS = ["done", "snooze", "dismiss"] + +# Reward is NOT a fixed map anymore — it depends on action + simulated dwell time. +# Use infer_reward() to compute the final reward after simulating dwell. +_BASE_REWARDS: dict[str, float] = { + "done": 1.0, # placeholder; real reward computed from dwell + "snooze": 0.1, + "dismiss": -1.0, +} + + +def infer_reward(action: str, dwell_ms: int) -> float: + """Mirror of production inferReward() in recommender.ts.""" + if action == "dismiss": + return -1.0 + if action == "snooze": + return 0.1 + # done — dwell-based + if dwell_ms < 15_000: + return -0.3 # stale / reflex done + if dwell_ms < 120_000: + return 1.0 # magic zone + if dwell_ms < 600_000: + return 0.6 # good + return 0.3 # eventually done + +_HOUR_PERIODS = { + (5, 10): "morning", + (10, 14): "midday", + (14, 18): "afternoon", + (18, 22): "evening", +} + + +def _period(hour: int) -> str: + for (lo, hi), name in _HOUR_PERIODS.items(): + if lo <= hour < hi: + return name + return "night" + + +# ── Deterministic judge ──────────────────────────────────────────────────── + +def _engagement_score(persona: Persona, tip: dict, hour: int) -> float: + """0–1 score of how well this tip fits this persona right now.""" + features = tip.get("features", {}) + priority = features.get("priority", 1) + is_overdue = features.get("is_overdue", False) + + p = 0.35 + priority_norm = (priority - 1) / 3.0 + p += (priority_norm - 0.5) * persona.prefers_high_priority * 0.4 + if is_overdue: + p += (persona.prefers_overdue - 0.5) * 0.3 + + is_morning = 5 <= hour < 10 + is_evening = 18 <= hour < 22 + if persona.morning_active and is_morning: + p += 0.15 + elif persona.evening_active and is_evening: + p += 0.15 + elif persona.morning_active and not is_morning and not is_evening: + p -= 0.10 + elif persona.evening_active and not is_evening and not is_morning: + p -= 0.10 + + return max(0.05, min(0.90, p)) + + +def _simulate_dwell_ms(engagement: float, rng: random.Random) -> int: + """ + Simulate how many milliseconds the user takes to act on a tip. + + High engagement → quick action (magic zone, 15s–2min). + Medium engagement → slower (2–10min). + Low engagement → very slow (>10min) — tip helped eventually but not 'magic'. + For snooze/dismiss the dwell doesn't affect reward; return a short value. + """ + if engagement >= 0.70: + # Strong match — magic zone: 15s–90s + return rng.randint(15_000, 90_000) + elif engagement >= 0.50: + # Moderate match — good zone: 2–8min + return rng.randint(120_000, 480_000) + else: + # Weak match but still done — eventually: 10–30min + return rng.randint(600_000, 1_800_000) + + +def _rule_judge(persona: Persona, tip: dict, hour: int, rng: random.Random) -> tuple[str, int]: + """Return (action, dwell_ms) based on persona preferences and task features.""" + engagement = _engagement_score(persona, tip, hour) + + r = rng.random() + if r < engagement * 0.55: + # done — dwell depends on engagement + dwell = _simulate_dwell_ms(engagement, rng) + return "done", dwell + elif r < engagement: + return "snooze", rng.randint(3_000, 20_000) + else: + return "dismiss", rng.randint(1_000, 5_000) + + +# ── LLM judge ───────────────────────────────────────────────────────────── + +_anthropic_client = None + +def _get_client(): + global _anthropic_client + if _anthropic_client is None: + try: + import anthropic # type: ignore + key = os.environ.get("ANTHROPIC_API_KEY", "") + if key: + _anthropic_client = anthropic.Anthropic(api_key=key) + except ImportError: + pass + return _anthropic_client + + +def _llm_judge( + persona: Persona, tip: dict, hour: int, day_of_week: int, rng: random.Random, +) -> tuple[str, int]: + client = _get_client() + if client is None: + return _rule_judge(persona, tip, hour, rng) + + features = tip.get("features", {}) + priority = features.get("priority", 1) + is_overdue = features.get("is_overdue", False) + age_days = features.get("task_age_days", 0) + + priority_label = {1: "low", 2: "normal", 3: "high", 4: "urgent"}.get(priority, "normal") + overdue_str = f", overdue by {age_days:.0f} day(s)" if is_overdue else "" + days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + day_str = days[day_of_week % 7] + + prompt = ( + f"You are simulating how a specific user reacts to a task recommendation app.\n\n" + f"User persona: {persona.name}\n" + f"Persona: {persona.description}\n\n" + f'Recommended task: "{tip.get("content", "Unknown task")}"\n' + f"Task: priority={priority_label}{overdue_str}\n" + f"Current time: {_period(hour)} ({hour}:00, {day_str})\n\n" + f"How does this user react? Reply with exactly one word: done | snooze | dismiss\n\n" + f"- done: acts on this tip (marks task complete)\n" + f"- snooze: acknowledges but not now\n" + f"- dismiss: ignores or rejects it" + ) + + try: + message = client.messages.create( + model="claude-haiku-4-5-20251001", + max_tokens=10, + messages=[{"role": "user", "content": prompt}], + ) + raw = message.content[0].text.strip().lower().split()[0] + action = raw if raw in ACTIONS else _rule_judge(persona, tip, hour, rng)[0] + except Exception: + action, _ = _rule_judge(persona, tip, hour, rng) + + # Simulate dwell based on engagement level + engagement = _engagement_score(persona, tip, hour) + dwell = _simulate_dwell_ms(engagement, rng) if action == "done" else rng.randint(2_000, 15_000) + return action, dwell + + +# ── Public API ───────────────────────────────────────────────────────────── + +def judge( + persona: Persona, + tip: dict, + hour: int, + day_of_week: int, + rng: random.Random, + use_llm: bool = True, +) -> tuple[str, int, float]: + """Return (action, dwell_ms, reward). + + action — 'done' | 'snooze' | 'dismiss' + dwell_ms — simulated milliseconds between tip appearance and user action + reward — inferred from action + dwell_ms via infer_reward() + """ + if use_llm and os.environ.get("ANTHROPIC_API_KEY"): + action, dwell_ms = _llm_judge(persona, tip, hour, day_of_week, rng) + else: + action, dwell_ms = _rule_judge(persona, tip, hour, rng) + + return action, dwell_ms, infer_reward(action, dwell_ms) diff --git a/ml/experiments/sim/personas.py b/ml/experiments/sim/personas.py new file mode 100644 index 0000000..8c43805 --- /dev/null +++ b/ml/experiments/sim/personas.py @@ -0,0 +1,79 @@ +"""Synthetic user personas for simulation.""" + +from dataclasses import dataclass + + +@dataclass +class Persona: + name: str + description: str + # Feature preference weights — used by deterministic judge + prefers_high_priority: float # 0–1: scales response to priority + prefers_overdue: float # 0–1: scales response to overdue tasks + morning_active: bool # higher engagement hours 6–10 + evening_active: bool # higher engagement hours 18–22 + recency_bias: float # 0–1: prefers recently-due tasks + + +PERSONAS: list[Persona] = [ + Persona( + name="deadline-driven", + description=( + "Responds urgently to overdue and high-priority tasks. " + "Most active in the morning. Dismisses low-priority tips." + ), + prefers_high_priority=0.9, + prefers_overdue=0.85, + morning_active=True, + evening_active=False, + recency_bias=0.3, + ), + Persona( + name="evening-relaxed", + description=( + "Reviews tasks in the evenings. Neutral on priority. " + "Snoozes morning recommendations." + ), + prefers_high_priority=0.5, + prefers_overdue=0.4, + morning_active=False, + evening_active=True, + recency_bias=0.5, + ), + Persona( + name="low-priority-first", + description=( + "Clears small tasks first. Snoozes urgent items until deadline. " + "Morning person." + ), + prefers_high_priority=0.2, + prefers_overdue=0.6, + morning_active=True, + evening_active=False, + recency_bias=0.7, + ), + Persona( + name="consistent-responder", + description=( + "Engages consistently across hours and days. " + "Acts on helpful tips regardless of priority." + ), + prefers_high_priority=0.6, + prefers_overdue=0.6, + morning_active=True, + evening_active=True, + recency_bias=0.5, + ), + Persona( + name="overdue-ignorer", + description=( + "Avoids overdue tasks (stress avoidance). " + "Focuses on future-due, high-priority items. Evening person." + ), + prefers_high_priority=0.8, + prefers_overdue=0.1, + morning_active=False, + evening_active=True, + recency_bias=0.2, + ), +] diff --git a/ml/experiments/sim/runner.py b/ml/experiments/sim/runner.py new file mode 100644 index 0000000..da65079 --- /dev/null +++ b/ml/experiments/sim/runner.py @@ -0,0 +1,527 @@ +""" +oO simulation runner — compares two recommendation policies. + +Judge modes: + rule Deterministic persona-based rules (default, no external deps) + llm Claude Haiku via Anthropic API (requires ANTHROPIC_API_KEY) + claude-code Two-phase: Claude Code acts as the judge (you are the judge) + +Usage — rule/llm (single pass): + python runner.py --n-users 5 --n-rounds 10 --no-llm + python runner.py --n-users 5 --n-rounds 10 + +Usage — claude-code judge (two phases): + # Phase 1: score candidates, write judgment requests + python runner.py --judge claude-code --phase score \\ + --n-users 5 --n-rounds 10 --out /tmp/oo-cc-sim.json + + # (Claude Code reads /tmp/oo-cc-sim-requests.json and writes /tmp/oo-cc-sim-responses.json) + + # Phase 2: apply responses, run rewards, produce results + python runner.py --judge claude-code --phase reward --plan /tmp/oo-cc-sim-plan.json \\ + --out /tmp/oo-cc-sim.json +""" + +from __future__ import annotations + +import argparse +import json +import random +import sys +import time +import uuid +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +import httpx + +from llm_judge import ACTIONS, infer_reward, judge +from personas import PERSONAS, Persona +from task_generator import generate_task_pool + +POLICY_SCORE_ENDPOINTS: dict[str, str] = { + "linucb-v1": "/score", + "egreedy-v1": "/score/egreedy", +} +POLICY_REWARD_ENDPOINTS: dict[str, str] = { + "linucb-v1": "/reward", + "egreedy-v1": "/reward/egreedy", +} + + +def _call_score( + client: httpx.Client, ml_url: str, policy: str, + user_id: str, tasks: list[dict], hour: int, dow: int, +) -> dict | None: + endpoint = POLICY_SCORE_ENDPOINTS.get(policy, "/score") + body = { + "user_id": user_id, + "candidates": [ + { + "id": t["id"], "content": t["content"], "source": t["source"], + "source_id": None, + "features": { + "hour_of_day": hour, + "is_overdue": t["features"]["is_overdue"], + "task_age_days": t["features"]["task_age_days"], + "priority": t["features"]["priority"], + }, + } + for t in tasks + ], + "context": {"hour_of_day": hour, "day_of_week": dow}, + } + try: + r = client.post(f"{ml_url}{endpoint}", json=body, timeout=5.0) + r.raise_for_status() + return r.json() + except Exception as e: + print(f" [warn] score {policy}: {e}", file=sys.stderr) + return None + + +def _call_reward( + client: httpx.Client, ml_url: str, policy: str, + user_id: str, tip_id: str, reward: float, features: dict, + day_of_week: int = 0, +) -> None: + endpoint = POLICY_REWARD_ENDPOINTS.get(policy, "/reward") + try: + client.post( + f"{ml_url}{endpoint}", + json={"user_id": user_id, "tip_id": tip_id, "reward": reward, + "features": features, "day_of_week": day_of_week}, + timeout=5.0, + ) + except Exception as e: + print(f" [warn] reward {policy}: {e}", file=sys.stderr) + + +# ── Standard single-pass runner (rule / llm modes) ───────────────────────── + +def run_simulation( + n_users: int, n_rounds: int, tasks_per_round: int, + ml_url: str, policies: list[str], use_llm: bool, seed: int, +) -> dict: + rng = random.Random(seed) + run_id = str(uuid.uuid4())[:8] + started_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + + user_personas = [ + (f"sim-{run_id}-u{i}", PERSONAS[i % len(PERSONAS)]) + for i in range(n_users) + ] + + acc: dict[str, dict] = { + p: { + "total_reward": 0.0, "n_pulls": 0, + "cumulative_rewards": [], + "action_counts": {a: 0 for a in ACTIONS}, + } + for p in policies + } + events: list[dict] = [] + + with httpx.Client(trust_env=False) as client: + for rnd in range(n_rounds): + hour = rng.randint(6, 22) + dow = rng.randint(0, 6) + round_rewards = {p: 0.0 for p in policies} + + for user_id, persona in user_personas: + seed_tasks = rnd * 997 + abs(hash(user_id)) % 997 + tasks = generate_task_pool(n=tasks_per_round, seed=seed_tasks) + + for policy in policies: + p_user = f"{user_id}-{policy}" + scored = _call_score(client, ml_url, policy, p_user, tasks, hour, dow) + if not scored: + continue + tip_id = scored.get("tip_id") + tip = next((t for t in tasks if t["id"] == tip_id), None) + if not tip: + continue + + action, dwell_ms, reward = judge(persona, tip, hour, dow, rng, use_llm=use_llm) + _call_reward(client, ml_url, policy, p_user, tip_id, reward, { + "hour_of_day": hour, + "is_overdue": tip["features"]["is_overdue"], + "task_age_days": tip["features"]["task_age_days"], + "priority": tip["features"]["priority"], + }, day_of_week=dow) + + acc[policy]["total_reward"] += reward + acc[policy]["n_pulls"] += 1 + acc[policy]["action_counts"][action] += 1 + round_rewards[policy] += reward + events.append({ + "round": rnd, "user_id": user_id, "persona": persona.name, + "policy": policy, "tip_content": tip["content"], + "priority": tip["features"]["priority"], + "is_overdue": tip["features"]["is_overdue"], + "action": action, "dwell_ms": dwell_ms, "reward": reward, + "hour": hour, "day_of_week": dow, + }) + + for p in policies: + prev = acc[p]["cumulative_rewards"][-1] if acc[p]["cumulative_rewards"] else 0.0 + acc[p]["cumulative_rewards"].append(prev + round_rewards[p]) + + mode = "llm" if use_llm else "rule" + print(f" Round {rnd+1:>3}/{n_rounds} [{mode}] " + " ".join( + f"{p}={acc[p]['cumulative_rewards'][-1]:+.2f}" for p in policies + )) + + return _build_result(run_id, started_at, policies, acc, events, + n_users, n_rounds, tasks_per_round, use_llm, seed) + + +# ── Claude Code judge — phase 1: score ───────────────────────────────────── + +def run_score_phase( + n_users: int, n_rounds: int, tasks_per_round: int, + ml_url: str, policies: list[str], seed: int, out_path: str, +) -> None: + """Score all candidates and write judgment requests for Claude Code.""" + rng = random.Random(seed) + run_id = str(uuid.uuid4())[:8] + started_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + + user_personas = [ + (f"sim-{run_id}-u{i}", PERSONAS[i % len(PERSONAS)]) + for i in range(n_users) + ] + + plan_rounds: list[dict] = [] + judgment_requests: list[dict] = [] + + print(f"[Phase 1] Scoring {n_rounds} rounds × {n_users} users × {len(policies)} policies…") + + with httpx.Client(trust_env=False) as client: + for rnd in range(n_rounds): + hour = rng.randint(6, 22) + dow = rng.randint(0, 6) + round_sessions: list[dict] = [] + + for user_id, persona in user_personas: + seed_tasks = rnd * 997 + abs(hash(user_id)) % 997 + tasks = generate_task_pool(n=tasks_per_round, seed=seed_tasks) + + for policy in policies: + p_user = f"{user_id}-{policy}" + scored = _call_score(client, ml_url, policy, p_user, tasks, hour, dow) + if not scored: + continue + tip_id = scored.get("tip_id") + tip = next((t for t in tasks if t["id"] == tip_id), None) + if not tip: + continue + + req_id = f"r{rnd}_{user_id.split('-')[-1]}_{policy}" + round_sessions.append({ + "req_id": req_id, + "p_user": p_user, + "policy": policy, + "user_id": user_id, + "persona_name": persona.name, + "tip_id": tip_id, + "tip_features": tip["features"], + "tip_content": tip["content"], + "ml_score": scored.get("score"), + }) + + judgment_requests.append({ + "id": req_id, + "round": rnd, + "hour": hour, + "day_of_week": dow, + "policy": policy, + "persona_name": persona.name, + "persona_description": persona.description, + "tip_content": tip["content"], + "priority": tip["features"]["priority"], + "is_overdue": tip["features"]["is_overdue"], + "age_days": tip["features"]["task_age_days"], + "ml_score": scored.get("score"), + }) + + plan_rounds.append({ + "round": rnd, "hour": hour, "dow": dow, + "sessions": round_sessions, + }) + print(f" Round {rnd+1:>3}/{n_rounds}: {len(round_sessions)} sessions scored") + + plan = { + "run_id": run_id, + "started_at": started_at, + "config": { + "n_users": n_users, "n_rounds": n_rounds, + "tasks_per_round": tasks_per_round, "policies": policies, + "use_llm": False, "seed": seed, + }, + "user_personas": [ + {"user_id": uid, "persona_name": p.name, "persona_description": p.description} + for uid, p in user_personas + ], + "rounds": plan_rounds, + } + + base = out_path.replace(".json", "") + plan_path = f"{base}-plan.json" + requests_path = f"{base}-requests.json" + responses_path = f"{base}-responses.json" + + Path(plan_path).write_text(json.dumps(plan, indent=2)) + Path(requests_path).write_text(json.dumps(judgment_requests, indent=2)) + + print() + print("=" * 60) + print(f"Phase 1 complete — {len(judgment_requests)} judgment requests.") + print() + print(f" Requests : {requests_path}") + print(f" Plan : {plan_path}") + print() + print('Claude Code: read the requests file, judge each tip for the persona,') + print(f'then write your responses to: {responses_path}') + print() + print('Response format: { "": "" | { "action": "", "dwell_ms": } }') + print('Valid actions: done | snooze | dismiss') + print() + print('For "done", optionally specify dwell_ms (ms between tip appearing and user acting):') + print(' { "r0_u0_linucb-v1": { "action": "done", "dwell_ms": 45000 } } # magic zone') + print(' { "r0_u0_linucb-v1": "snooze" } # plain string also ok (uses default 60s dwell for done)') + print() + print('Reward is inferred from action + dwell_ms:') + print(' dismiss → -1.0') + print(' snooze → 0.1') + print(' done < 15s → -0.3 (stale task)') + print(' done 15s–2min → 1.0 (magic!)') + print(' done 2–10min → 0.6 (good)') + print(' done > 10min → 0.3 (eventually)') + print() + print('Then run Phase 2:') + print(f' python runner.py --judge claude-code --phase reward \\') + print(f' --plan {plan_path} --out {out_path}') + + +# ── Claude Code judge — phase 2: reward ──────────────────────────────────── + +def run_reward_phase(plan_path: str, out_path: str, ml_url: str) -> dict: + """Apply Claude Code judgments, send reward signals, compute metrics.""" + plan = json.loads(Path(plan_path).read_text()) + base = plan_path.replace("-plan.json", "") + responses_path = f"{base}-responses.json" + + if not Path(responses_path).exists(): + print(f"ERROR: responses file not found: {responses_path}", file=sys.stderr) + sys.exit(1) + + raw_responses = json.loads(Path(responses_path).read_text()) + + # Responses can be either { id: "action" } or { id: { action, dwell_ms } } + def _parse_response(v) -> tuple[str, int]: + if isinstance(v, dict): + return v["action"], int(v.get("dwell_ms", 60_000)) + return str(v), 60_000 # plain string → assume 60s dwell for "done" + + responses: dict[str, tuple[str, int]] = {k: _parse_response(v) for k, v in raw_responses.items()} + + invalid = {k: v[0] for k, v in responses.items() if v[0] not in ACTIONS} + if invalid: + print(f"ERROR: invalid actions in responses: {invalid}", file=sys.stderr) + sys.exit(1) + + policies: list[str] = plan["config"]["policies"] + acc: dict[str, dict] = { + p: { + "total_reward": 0.0, "n_pulls": 0, + "cumulative_rewards": [], + "action_counts": {a: 0 for a in ACTIONS}, + } + for p in policies + } + events: list[dict] = [] + persona_map = {u["user_id"]: u["persona_name"] for u in plan["user_personas"]} + missing_responses = 0 + + print(f"[Phase 2] Applying {len(responses)} judgments → reward calls…") + + with httpx.Client(trust_env=False) as client: + for rnd_data in plan["rounds"]: + rnd = rnd_data["round"] + round_rewards = {p: 0.0 for p in policies} + + for session in rnd_data["sessions"]: + req_id = session["req_id"] + resp = responses.get(req_id) + if not resp: + print(f" [warn] no response for {req_id}, defaulting to snooze") + action, dwell_ms = "snooze", 10_000 + missing_responses += 1 + else: + action, dwell_ms = resp + + reward = infer_reward(action, dwell_ms) + _call_reward( + client, ml_url, session["policy"], session["p_user"], + session["tip_id"], reward, + {"hour_of_day": rnd_data["hour"], **session["tip_features"]}, + day_of_week=rnd_data["dow"], + ) + + p = session["policy"] + acc[p]["total_reward"] += reward + acc[p]["n_pulls"] += 1 + acc[p]["action_counts"][action] += 1 + round_rewards[p] += reward + + events.append({ + "round": rnd, + "user_id": session["user_id"], + "persona": persona_map.get(session["user_id"], "?"), + "policy": p, + "tip_content": session["tip_content"], + "priority": session["tip_features"]["priority"], + "is_overdue": session["tip_features"]["is_overdue"], + "action": action, + "dwell_ms": dwell_ms, + "reward": reward, + "hour": rnd_data["hour"], + "day_of_week": rnd_data["dow"], + }) + + for p in policies: + prev = acc[p]["cumulative_rewards"][-1] if acc[p]["cumulative_rewards"] else 0.0 + acc[p]["cumulative_rewards"].append(prev + round_rewards[p]) + + print(f" Round {rnd+1:>3}/{plan['config']['n_rounds']} [cc] " + " ".join( + f"{p}={acc[p]['cumulative_rewards'][-1]:+.2f}" for p in policies + )) + + if missing_responses: + print(f" [warn] {missing_responses} requests had no response (defaulted to snooze)") + + cfg = plan["config"] + result = _build_result( + plan["run_id"], plan["started_at"], policies, acc, events, + cfg["n_users"], cfg["n_rounds"], cfg["tasks_per_round"], + use_llm=False, seed=cfg["seed"], + ) + result["judge_mode"] = "claude-code" + Path(out_path).write_text(json.dumps(result, indent=2)) + return result + + +# ── Shared result builder ─────────────────────────────────────────────────── + +def _build_result( + run_id: str, started_at: str, policies: list[str], + acc: dict, events: list[dict], + n_users: int, n_rounds: int, tasks_per_round: int, + use_llm: bool, seed: int, +) -> dict: + summary = { + p: { + "total_reward": acc[p]["total_reward"], + "mean_reward": ( + acc[p]["total_reward"] / acc[p]["n_pulls"] + if acc[p]["n_pulls"] > 0 else 0.0 + ), + "n_pulls": acc[p]["n_pulls"], + "cumulative_rewards": acc[p]["cumulative_rewards"], + "action_counts": acc[p]["action_counts"], + } + for p in policies + } + winner = max(policies, key=lambda p: summary[p]["total_reward"]) + + persona_breakdown: dict[str, dict] = {} + for ev in events: + pname = ev["persona"] + pol = ev["policy"] + persona_breakdown.setdefault(pname, {}).setdefault(pol, {"reward": 0.0, "n": 0}) + persona_breakdown[pname][pol]["reward"] += ev["reward"] + persona_breakdown[pname][pol]["n"] += 1 + + return { + "run_id": run_id, + "started_at": started_at, + "finished_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "config": { + "n_users": n_users, "n_rounds": n_rounds, + "tasks_per_round": tasks_per_round, "policies": policies, + "use_llm": use_llm, "seed": seed, + }, + "summary": summary, + "winner": winner, + "persona_breakdown": persona_breakdown, + "events": events, + } + + +# ── CLI ───────────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="oO simulation runner") + parser.add_argument("--judge", choices=["rule", "llm", "claude-code"], default="rule") + parser.add_argument("--phase", choices=["score", "reward"], default=None, + help="For --judge claude-code only") + parser.add_argument("--plan", default=None, + help="Plan file path (for --judge claude-code --phase reward)") + parser.add_argument("--n-users", type=int, default=5) + parser.add_argument("--n-rounds", type=int, default=20) + parser.add_argument("--tasks-per-round", type=int, default=8) + parser.add_argument("--ml-url", default="http://localhost:5001") + parser.add_argument("--policies", nargs="+", default=["linucb-v1", "egreedy-v1"]) + parser.add_argument("--no-llm", action="store_true", + help="Alias for --judge rule (backwards compat)") + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--out", default=None) + args = parser.parse_args() + + if args.no_llm: + args.judge = "rule" + + out_path = args.out or f"/tmp/oo-sim-{int(time.time())}.json" + + if args.judge == "claude-code": + if args.phase == "score": + run_score_phase( + n_users=args.n_users, n_rounds=args.n_rounds, + tasks_per_round=args.tasks_per_round, ml_url=args.ml_url, + policies=args.policies, seed=args.seed, out_path=out_path, + ) + elif args.phase == "reward": + if not args.plan: + print("ERROR: --plan is required for --phase reward", file=sys.stderr) + sys.exit(1) + result = run_reward_phase(args.plan, out_path, args.ml_url) + print() + print(f"Winner : {result['winner']}") + for p, s in result["summary"].items(): + print(f" {p:20s} total={s['total_reward']:+.2f} mean={s['mean_reward']:+.4f} pulls={s['n_pulls']}") + print(f"Results: {out_path}") + else: + print("ERROR: --judge claude-code requires --phase score or --phase reward", + file=sys.stderr) + sys.exit(1) + else: + use_llm = (args.judge == "llm") + print(f"oO simulation: {args.n_users} users × {args.n_rounds} rounds") + print(f"Policies : {args.policies}") + print(f"ML URL : {args.ml_url}") + print(f"Judge : {args.judge}") + print() + + result = run_simulation( + n_users=args.n_users, n_rounds=args.n_rounds, + tasks_per_round=args.tasks_per_round, ml_url=args.ml_url, + policies=args.policies, use_llm=use_llm, seed=args.seed, + ) + Path(out_path).write_text(json.dumps(result, indent=2)) + print() + print(f"Winner : {result['winner']}") + for p, s in result["summary"].items(): + print(f" {p:20s} total={s['total_reward']:+.2f} mean={s['mean_reward']:+.4f} pulls={s['n_pulls']}") + print(f"Results: {out_path}") diff --git a/ml/experiments/sim/task_generator.py b/ml/experiments/sim/task_generator.py new file mode 100644 index 0000000..687b43e --- /dev/null +++ b/ml/experiments/sim/task_generator.py @@ -0,0 +1,62 @@ +"""Generate synthetic task pools for simulation.""" + +from __future__ import annotations + +import random + +_TEMPLATES = [ + "Send weekly report to team", + "Review pull request #{n}", + "Schedule meeting with {name}", + "Update project documentation", + "Fix bug in authentication module", + "Prepare presentation for stakeholders", + "Call back {name}", + "Submit expense report", + "Review quarterly goals", + "Clean up inbox", + "Follow up on proposal to {name}", + "Complete onboarding checklist", + "Write tests for feature #{n}", + "Deploy hotfix to production", + "Respond to support ticket #{n}", + "Draft release notes", + "Update dependencies", + "Review design mockups", + "Archive old tickets", + "Check in with {name}", +] + +_NAMES = ["Alice", "Bob", "Carol", "David", "Eve", "Frank", "Grace"] + + +def generate_task_pool(n: int = 10, seed: int | None = None) -> list[dict]: + """Return n synthetic tasks with randomly sampled features.""" + rng = random.Random(seed) + + tasks = [] + for i in range(n): + priority = rng.choices([1, 2, 3, 4], weights=[0.3, 0.3, 0.25, 0.15])[0] + # age_days: most tasks fresh, a few stale + age_days = rng.choices( + [0.0, 0.5, 1.0, 3.0, 7.0, 14.0], + weights=[0.35, 0.20, 0.20, 0.12, 0.08, 0.05], + )[0] + rng.random() * 0.5 + # is_overdue only meaningful when age > 0 + is_overdue = age_days > 0.5 and rng.random() < 0.65 + + template = rng.choice(_TEMPLATES) + content = template.format(n=rng.randint(100, 999), name=rng.choice(_NAMES)) + + tasks.append({ + "id": f"sim:{i}", + "content": content, + "source": "sim", + "features": { + "is_overdue": is_overdue, + "task_age_days": age_days if is_overdue else 0.0, + "priority": priority, + }, + }) + + return tasks diff --git a/ml/serving/main.py b/ml/serving/main.py index 9df337c..8d6afd9 100644 --- a/ml/serving/main.py +++ b/ml/serving/main.py @@ -35,8 +35,10 @@ app = FastAPI(title="oO ML Serving", version="1.0.0") STATE_DIR = Path(os.getenv("STATE_DIR", "/tmp/oo-bandit-state")) STATE_DIR.mkdir(parents=True, exist_ok=True) -ALPHA = 1.0 # exploration coefficient -D = 5 # feature dimension +ALPHA = 1.0 # LinUCB exploration coefficient +D = 5 # LinUCB feature dimension +D7 = 7 # ε-greedy feature dimension (adds day-of-week cyclical encoding) +EPSILON = 0.1 # ε-greedy exploration rate FEATURE_HISTORY_SIZE = 100 # per-user ring buffer @@ -63,6 +65,8 @@ def build_feature_vector(features: dict) -> np.ndarray: # ── Per-user bandit state (disjoint LinUCB, global arm) ─────────────────── +# ── LinUCB state helpers ─────────────────────────────────────────────────── + def state_path(user_id: str) -> Path: safe = "".join(c if c.isalnum() else "_" for c in user_id) return STATE_DIR / f"{safe}.json" @@ -85,6 +89,37 @@ def save_state(user_id: str, A: np.ndarray, b: np.ndarray, meta: dict) -> None: p.write_text(json.dumps({"A": A.tolist(), "b": b.tolist(), "meta": meta})) +# ── ε-greedy state helpers (d=7, extended features) ─────────────────────── + +def build_feature_vector_7(features: dict, day_of_week: int = 0) -> np.ndarray: + """d=7: base 5 features + day-of-week cyclical encoding.""" + base = build_feature_vector(features) + dow_sin = math.sin(2 * math.pi * day_of_week / 7) + dow_cos = math.cos(2 * math.pi * day_of_week / 7) + return np.append(base, [dow_sin, dow_cos]) + + +def state7_path(user_id: str) -> Path: + safe = "".join(c if c.isalnum() else "_" for c in user_id) + return STATE_DIR / f"{safe}_egreedy.json" + + +def load_state7(user_id: str) -> tuple[np.ndarray, np.ndarray, dict]: + """Returns (A, b, meta) for ε-greedy d=7 policy.""" + p = state7_path(user_id) + if p.exists(): + raw = json.loads(p.read_text()) + A = np.array(raw["A"], dtype=np.float64) + b = np.array(raw["b"], dtype=np.float64) + return A, b, raw.get("meta", {}) + return np.identity(D7, dtype=np.float64), np.zeros(D7, dtype=np.float64), {} + + +def save_state7(user_id: str, A: np.ndarray, b: np.ndarray, meta: dict) -> None: + p = state7_path(user_id) + p.write_text(json.dumps({"A": A.tolist(), "b": b.tolist(), "meta": meta})) + + # ── API models ───────────────────────────────────────────────────────────── class CandidateFeatures(BaseModel): @@ -124,6 +159,7 @@ class RewardRequest(BaseModel): tip_id: str reward: float # +1 done, +0.5 helpful, 0 snooze, -0.5 not_helpful, -1 dismiss features: CandidateFeatures + day_of_week: int = 0 # included so egreedy can train dow features correctly class RewardResponse(BaseModel): @@ -209,12 +245,131 @@ def reward(req: RewardRequest) -> RewardResponse: return RewardResponse(ok=True) +@app.post("/score/egreedy", response_model=ScoreResponse) +def score_egreedy(req: ScoreRequest) -> ScoreResponse: + """ε-greedy policy with d=7 features (adds day-of-week encoding). + + Exploration: pick uniformly at random with probability ε. + Exploitation: pick argmax of linear payoff estimate θ·x. + Differs from LinUCB in: no UCB bonus, richer feature space. + """ + if not req.candidates: + raise HTTPException(status_code=422, detail="No candidates") + + A, b, meta = load_state7(req.user_id) + try: + A_inv = np.linalg.inv(A) + except np.linalg.LinAlgError: + A_inv = np.identity(D7, dtype=np.float64) + theta = A_inv @ b + + dow = req.context.day_of_week + exploring = np.random.random() < EPSILON + + if exploring: + chosen = req.candidates[np.random.randint(len(req.candidates))] + feat_dict = { + "hour_of_day": req.context.hour_of_day, + "is_overdue": chosen.features.is_overdue, + "task_age_days": chosen.features.task_age_days, + "priority": chosen.features.priority, + } + x = build_feature_vector_7(feat_dict, dow) + best_score = float(theta @ x) + best_id = chosen.id + else: + best_id = None + best_score = -float("inf") + feat_dict = {} + for candidate in req.candidates: + fd = { + "hour_of_day": req.context.hour_of_day, + "is_overdue": candidate.features.is_overdue, + "task_age_days": candidate.features.task_age_days, + "priority": candidate.features.priority, + } + x = build_feature_vector_7(fd, dow) + s = float(theta @ x) + if s > best_score: + best_score = s + best_id = candidate.id + feat_dict = fd + + history = get_feature_history(req.user_id) + history.append({ + "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "features": {**feat_dict, "day_of_week": dow, "exploring": exploring}, + "score": best_score, + "tip_id": best_id, + "policy": "egreedy-v1", + }) + + meta["pulls"] = meta.get("pulls", 0) + 1 + meta["explore_count"] = meta.get("explore_count", 0) + int(exploring) + meta["last_updated"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + save_state7(req.user_id, A, b, meta) + + return ScoreResponse(tip_id=best_id, score=best_score, policy="egreedy-v1") + + +@app.post("/reward/egreedy", response_model=RewardResponse) +def reward_egreedy(req: RewardRequest) -> RewardResponse: + """Update ε-greedy ridge estimator with observed reward.""" + A, b, meta = load_state7(req.user_id) + feat_dict = { + "hour_of_day": req.features.hour_of_day, + "is_overdue": req.features.is_overdue, + "task_age_days": req.features.task_age_days, + "priority": req.features.priority, + } + x = build_feature_vector_7(feat_dict, day_of_week=req.day_of_week) + A += np.outer(x, x) + b += req.reward * x + + meta["cumulative_reward"] = meta.get("cumulative_reward", 0.0) + req.reward + meta["reward_count"] = meta.get("reward_count", 0) + 1 + meta["last_updated"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + save_state7(req.user_id, A, b, meta) + return RewardResponse(ok=True) + + +@app.get("/stats/egreedy/{user_id}") +def stats_egreedy(user_id: str): + """ε-greedy policy stats — pulls, cumulative reward, θ vector.""" + A, b, meta = load_state7(user_id) + try: + theta = (np.linalg.inv(A) @ b).tolist() + except np.linalg.LinAlgError: + theta = [0.0] * D7 + + pulls = meta.get("pulls", 0) + cumulative_reward = meta.get("cumulative_reward", 0.0) + reward_count = meta.get("reward_count", 0) + explore_count = meta.get("explore_count", 0) + + return { + "user_id": user_id, + "policy": "egreedy-v1", + "pulls": pulls, + "reward_count": reward_count, + "cumulative_reward": cumulative_reward, + "estimated_mean_reward": cumulative_reward / reward_count if reward_count > 0 else 0.0, + "exploration_rate": explore_count / pulls if pulls > 0 else 0.0, + "theta": theta, + "feature_labels": ["hour_sin", "hour_cos", "is_overdue", "task_age", "priority", "dow_sin", "dow_cos"], + "last_updated": meta.get("last_updated"), + } + + @app.post("/reset/{user_id}", response_model=RewardResponse) def reset(user_id: str) -> RewardResponse: """Reset per-user bandit state (admin action).""" p = state_path(user_id) if p.exists(): p.unlink() + p7 = state7_path(user_id) + if p7.exists(): + p7.unlink() if user_id in _feature_history: _feature_history[user_id].clear() return RewardResponse(ok=True) diff --git a/ml/serving/package.json b/ml/serving/package.json index 8f6564a..ee3a674 100644 --- a/ml/serving/package.json +++ b/ml/serving/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "dev": ".venv/bin/uvicorn main:app --reload --port 8000", - "start": ".venv/bin/uvicorn main:app --port 8000" + "start": ".venv/bin/uvicorn main:app --port 8000", + "test": ".venv/bin/python -m pytest tests/ -v" } } diff --git a/ml/serving/requirements-dev.txt b/ml/serving/requirements-dev.txt new file mode 100644 index 0000000..c67da83 --- /dev/null +++ b/ml/serving/requirements-dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt +pytest==8.3.5 +pytest-asyncio==0.24.0 +httpx==0.28.1 diff --git a/ml/serving/requirements.txt b/ml/serving/requirements.txt index 3bd7f2f..05f3cdd 100644 --- a/ml/serving/requirements.txt +++ b/ml/serving/requirements.txt @@ -2,3 +2,5 @@ fastapi==0.115.6 uvicorn[standard]==0.32.1 pydantic==2.10.4 numpy>=1.26.0 +httpx>=0.27.0 +anthropic>=0.40.0 diff --git a/ml/serving/tests/__init__.py b/ml/serving/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ml/serving/tests/test_score.py b/ml/serving/tests/test_score.py new file mode 100644 index 0000000..9111f0a --- /dev/null +++ b/ml/serving/tests/test_score.py @@ -0,0 +1,261 @@ +""" +Unit tests for ml/serving — feature building and scoring contract. +Run with: pytest ml/serving/tests/ +""" +import math +import pytest +from httpx import AsyncClient, ASGITransport + +from main import app, build_feature_vector + + +class TestFeatureVector: + def test_shape(self): + v = build_feature_vector({"hour_of_day": 8, "is_overdue": True, "task_age_days": 3, "priority": 3}) + assert v.shape == (5,) + + def test_hour_encoding_noon(self): + v = build_feature_vector({"hour_of_day": 12}) + # sin(2π * 12/24) = sin(π) ≈ 0 + assert abs(v[0]) < 1e-10 + # cos(2π * 12/24) = cos(π) = -1 + assert abs(v[1] - (-1.0)) < 1e-10 + + def test_hour_encoding_midnight(self): + v = build_feature_vector({"hour_of_day": 0}) + # sin(0) = 0 + assert abs(v[0]) < 1e-10 + # cos(0) = 1 + assert abs(v[1] - 1.0) < 1e-10 + + def test_hour_encoding_6am(self): + v = build_feature_vector({"hour_of_day": 6}) + # sin(2π * 6/24) = sin(π/2) = 1 + assert abs(v[0] - 1.0) < 1e-10 + # cos(π/2) = 0 + assert abs(v[1]) < 1e-10 + + def test_age_clipped_at_30(self): + v_long = build_feature_vector({"task_age_days": 100}) + v_cap = build_feature_vector({"task_age_days": 30}) + assert v_long[3] == v_cap[3] == 1.0 + + def test_age_zero(self): + v = build_feature_vector({"task_age_days": 0}) + assert v[3] == pytest.approx(0.0) + + def test_age_15_days_normalised(self): + v = build_feature_vector({"task_age_days": 15}) + assert v[3] == pytest.approx(0.5) + + def test_priority_normalised(self): + v1 = build_feature_vector({"priority": 1}) + v4 = build_feature_vector({"priority": 4}) + assert v1[4] == pytest.approx(0.0) + assert v4[4] == pytest.approx(1.0) + + def test_priority_2_and_3(self): + v2 = build_feature_vector({"priority": 2}) + v3 = build_feature_vector({"priority": 3}) + assert v2[4] == pytest.approx(1 / 3) + assert v3[4] == pytest.approx(2 / 3) + + def test_is_overdue_true(self): + v = build_feature_vector({"is_overdue": True}) + assert v[2] == 1.0 + + def test_is_overdue_false(self): + v = build_feature_vector({"is_overdue": False}) + assert v[2] == 0.0 + + def test_defaults_when_no_keys(self): + v = build_feature_vector({}) + # hour=12 → sin(π)≈0, cos(π)=-1 + assert abs(v[0]) < 1e-10 + assert abs(v[1] - (-1.0)) < 1e-10 + assert v[2] == 0.0 # is_overdue=False + assert v[3] == 0.0 # task_age_days=0 + assert v[4] == 0.0 # priority=1 → (1-1)/3=0 + + +@pytest.mark.asyncio +async def test_health(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + r = await client.get("/health") + assert r.status_code == 200 + assert r.json()["ok"] is True + + +@pytest.mark.asyncio +async def test_score_returns_a_candidate(): + payload = { + "user_id": "test-user", + "candidates": [ + {"id": "t:1", "content": "Task A", "source": "todoist", "source_id": "1", + "features": {"is_overdue": True, "task_age_days": 2, "priority": 3}}, + {"id": "t:2", "content": "Task B", "source": "todoist", "source_id": "2", + "features": {"is_overdue": False, "task_age_days": 0, "priority": 1}}, + ], + "context": {"hour_of_day": 9, "day_of_week": 1}, + } + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + r = await client.post("/score", json=payload) + assert r.status_code == 200 + body = r.json() + assert body["tip_id"] in {"t:1", "t:2"} + assert "policy" in body + assert body["policy"] == "linucb-v1" + assert isinstance(body["score"], float) + + +@pytest.mark.asyncio +async def test_score_single_candidate_always_selected(): + """With a single candidate there is no choice — it must be returned.""" + payload = { + "user_id": "solo-user", + "candidates": [ + {"id": "only:1", "content": "Only task", "source": "todoist", + "features": {"is_overdue": False, "task_age_days": 0, "priority": 1}}, + ], + "context": {"hour_of_day": 10, "day_of_week": 0}, + } + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + r = await client.post("/score", json=payload) + assert r.status_code == 200 + assert r.json()["tip_id"] == "only:1" + + +@pytest.mark.asyncio +async def test_score_empty_candidates_returns_422(): + payload = {"user_id": "u", "candidates": [], "context": {"hour_of_day": 9, "day_of_week": 1}} + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + r = await client.post("/score", json=payload) + assert r.status_code == 422 + + +@pytest.mark.asyncio +async def test_reward_accepted(): + payload = { + "user_id": "reward-user", + "tip_id": "t:1", + "reward": 1.0, + "features": {"hour_of_day": 9, "is_overdue": True, "task_age_days": 2, "priority": 3}, + } + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + r = await client.post("/reward", json=payload) + assert r.status_code == 200 + assert r.json()["ok"] is True + + +@pytest.mark.asyncio +async def test_reward_updates_stats(): + """Posting a reward should increase cumulative_reward in /stats.""" + user_id = "reward-stats-user" + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + r0 = await client.get(f"/stats/{user_id}") + before = r0.json()["cumulative_reward"] + + await client.post("/reward", json={ + "user_id": user_id, + "tip_id": "tip:x", + "reward": 1.0, + "features": {"hour_of_day": 8, "is_overdue": False, "task_age_days": 0, "priority": 2}, + }) + r1 = await client.get(f"/stats/{user_id}") + assert r1.json()["cumulative_reward"] == pytest.approx(before + 1.0) + + +@pytest.mark.asyncio +async def test_score_increments_pulls(): + user_id = "pull-counter-user" + payload = { + "user_id": user_id, + "candidates": [ + {"id": "t:p1", "content": "Pull task", "source": "todoist", + "features": {"is_overdue": False, "task_age_days": 1, "priority": 2}}, + ], + "context": {"hour_of_day": 10, "day_of_week": 2}, + } + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + r0 = await client.get(f"/stats/{user_id}") + pulls_before = r0.json()["pulls"] + + await client.post("/score", json=payload) + await client.post("/score", json=payload) + + r1 = await client.get(f"/stats/{user_id}") + assert r1.json()["pulls"] == pulls_before + 2 + + +@pytest.mark.asyncio +async def test_reset_clears_state(): + user_id = "reset-user" + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + # Score once to build state + await client.post("/score", json={ + "user_id": user_id, + "candidates": [ + {"id": "t:r", "content": "Reset task", "source": "todoist", + "features": {"is_overdue": True, "task_age_days": 5, "priority": 4}}, + ], + "context": {"hour_of_day": 14, "day_of_week": 3}, + }) + r_reset = await client.post(f"/reset/{user_id}") + assert r_reset.json()["ok"] is True + + r_stats = await client.get(f"/stats/{user_id}") + assert r_stats.json()["pulls"] == 0 + + +@pytest.mark.asyncio +async def test_features_endpoint_returns_history(): + user_id = "features-user" + payload = { + "user_id": user_id, + "candidates": [ + {"id": "t:f1", "content": "Feature task", "source": "todoist", + "features": {"is_overdue": False, "task_age_days": 0, "priority": 1}}, + ], + "context": {"hour_of_day": 7, "day_of_week": 0}, + } + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + await client.post("/score", json=payload) + r = await client.get(f"/features/{user_id}") + body = r.json() + assert r.status_code == 200 + assert "history" in body + assert len(body["history"]) >= 1 + entry = body["history"][-1] + assert "ts" in entry + assert "score" in entry + assert "tip_id" in entry + + +@pytest.mark.asyncio +async def test_stats_for_fresh_user(): + """A user with no history should return zero/default stats without error.""" + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + r = await client.get("/stats/brand-new-user-xyz-abc") + body = r.json() + assert r.status_code == 200 + assert body["pulls"] == 0 + assert body["cumulative_reward"] == 0.0 + assert body["estimated_mean_reward"] == 0.0 + + +@pytest.mark.asyncio +async def test_reward_negative_value(): + """Dismissing a tip should decrease cumulative_reward.""" + user_id = "dismiss-user-neg" + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + r0 = await client.get(f"/stats/{user_id}") + before = r0.json()["cumulative_reward"] + + await client.post("/reward", json={ + "user_id": user_id, + "tip_id": "t:neg", + "reward": -1.0, + "features": {"hour_of_day": 20, "is_overdue": False, "task_age_days": 0, "priority": 1}, + }) + r1 = await client.get(f"/stats/{user_id}") + assert r1.json()["cumulative_reward"] == pytest.approx(before - 1.0) diff --git a/packages/shared-types/package.json b/packages/shared-types/package.json index 7ded076..332cf2f 100644 --- a/packages/shared-types/package.json +++ b/packages/shared-types/package.json @@ -12,10 +12,14 @@ }, "scripts": { "build": "tsc", + "test": "vitest run", + "test:watch": "vitest", "type-check": "tsc --noEmit", "clean": "rm -rf dist" }, "devDependencies": { - "typescript": "^5.7.3" + "@vitest/coverage-v8": "^4.1.4", + "typescript": "^5.7.3", + "vitest": "^4.1.4" } } diff --git a/packages/shared-types/src/__tests__/tip.test.ts b/packages/shared-types/src/__tests__/tip.test.ts new file mode 100644 index 0000000..7157eb1 --- /dev/null +++ b/packages/shared-types/src/__tests__/tip.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; +import type { Tip, TipFeedback, RecommendResponse } from '../index.js'; + +describe('Tip type contract', () => { + it('accepts a valid Tip object', () => { + const tip: Tip = { + id: 'todoist:123', + content: 'Finish the report', + source: 'todoist', + sourceId: '123', + createdAt: new Date().toISOString(), + }; + expect(tip.source).toBe('todoist'); + }); + + it('accepts advice source without sourceId', () => { + const tip: Tip = { + id: 'advice:abc', + content: 'Take a break', + source: 'advice', + createdAt: new Date().toISOString(), + }; + expect(tip.sourceId).toBeUndefined(); + }); + + it('RecommendResponse wraps a Tip', () => { + const res: RecommendResponse = { + tip: { id: 'x', content: 'Do it', source: 'todoist', createdAt: '' }, + }; + expect(res.tip.id).toBe('x'); + }); + + it('TipFeedback allows valid actions', () => { + const actions: TipFeedback['action'][] = ['done', 'dismiss', 'snooze']; + for (const action of actions) { + const fb: TipFeedback = { action }; + expect(fb.action).toBe(action); + } + }); +}); diff --git a/packages/shared-types/vitest.config.ts b/packages/shared-types/vitest.config.ts new file mode 100644 index 0000000..fcc89cc --- /dev/null +++ b/packages/shared-types/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + exclude: ['dist/**', 'node_modules/**'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c76a88b..bd52ea9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,21 +18,36 @@ importers: specifier: ^5.7.3 version: 5.9.3 - apps/web: + apps/admin: dependencies: '@oo/shared-types': specifier: workspace:* version: link:../../packages/shared-types + '@tanstack/react-table': + specifier: ^8.20.5 + version: 8.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tremor/react': + specifier: ^3.18.3 + version: 3.18.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + marked: + specifier: ^14.1.4 + version: 14.1.4 next: specifier: ^15.1.6 - version: 15.5.15(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 15.5.15(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: specifier: ^19.0.0 version: 19.2.5 react-dom: specifier: ^19.0.0 version: 19.2.5(react@19.2.5) + recharts: + specifier: ^2.15.3 + version: 2.15.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5) devDependencies: + '@types/marked': + specifier: ^6.0.0 + version: 6.0.0 '@types/node': specifier: ^22.10.5 version: 22.19.17 @@ -42,17 +57,84 @@ importers: '@types/react-dom': specifier: ^19.0.0 version: 19.2.3(@types/react@19.2.14) + autoprefixer: + specifier: ^10.4.20 + version: 10.5.0(postcss@8.5.9) + postcss: + specifier: ^8.5.1 + version: 8.5.9 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.19(tsx@4.21.0) typescript: specifier: ^5.7.3 version: 5.9.3 + apps/web: + dependencies: + '@oo/shared-types': + specifier: workspace:* + version: link:../../packages/shared-types + next: + specifier: ^15.1.6 + version: 15.5.15(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: + specifier: ^19.0.0 + version: 19.2.5 + react-dom: + specifier: ^19.0.0 + version: 19.2.5(react@19.2.5) + devDependencies: + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) + '@types/node': + specifier: ^22.10.5 + version: 22.19.17 + '@types/react': + specifier: ^19.0.0 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)) + '@vitest/coverage-v8': + specifier: ^4.1.4 + version: 4.1.4(vitest@4.1.4) + jsdom: + specifier: ^29.0.2 + version: 29.0.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.4 + version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)) + ml/serving: {} packages/shared-types: devDependencies: + '@vitest/coverage-v8': + specifier: ^4.1.4 + version: 4.1.4(vitest@4.1.4) typescript: specifier: ^5.7.3 version: 5.9.3 + vitest: + specifier: ^4.1.4 + version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)) services/api: dependencies: @@ -114,6 +196,9 @@ importers: '@types/web-push': specifier: ^3.6.4 version: 3.6.4 + '@vitest/coverage-v8': + specifier: ^4.1.4 + version: 4.1.4(vitest@4.1.4) drizzle-kit: specifier: ^0.30.4 version: 0.30.6 @@ -123,15 +208,111 @@ importers: typescript: specifier: ^5.7.3 version: 5.9.3 + vitest: + specifier: ^4.1.4 + version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0)) packages: + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@asamuzakjp/css-color@5.1.10': + resolution: {integrity: sha512-02OhhkKtgNRuicQ/nF3TRnGsxL9wp0r3Y7VlKWyOHHGmGyvXv03y+PnymU8FKFJMTjIr1Bk8U2g1HWSLrpAHww==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.0.9': + resolution: {integrity: sha512-r3ElRr7y8ucyN2KdICwGsmj19RoN13CLCa/pvGydghWK6ZzeKQ+TcDjVdtEZz2ElpndM5jXw//B9CEee0mWnVg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -566,6 +747,55 @@ packages: cpu: [x64] os: [win32] + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@1.3.0': + resolution: {integrity: sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.19.2': + resolution: {integrity: sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@headlessui/react@2.2.0': + resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -719,6 +949,34 @@ packages: cpu: [x64] os: [win32] + '@internationalized/date@3.12.1': + resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==} + + '@internationalized/number@3.6.6': + resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==} + + '@internationalized/string@3.2.8': + resolution: {integrity: sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.3': + resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@next/env@15.5.15': resolution: {integrity: sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==} @@ -774,12 +1032,208 @@ packages: cpu: [x64] os: [win32] + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@petamoriken/float16@3.9.3': resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + + '@react-aria/focus@3.22.0': + resolution: {integrity: sha512-ZfDOVuVhqDsM9mkNji3QUZ/d40JhlVgXrDkrfXylM1035QCrcTHN7m2DpbE95sU2A8EQb4wikvt5jM6K/73BPg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.28.0': + resolution: {integrity: sha512-OXwdU1EWFdMxmr/K1CXNGJzmNlCClByb+PuCaqUyzBymHPCGVhawirLIon/CrIN5psh3AiWpHSh4H0WeJdVpng==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.34.0': + resolution: {integrity: sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/react-virtual@3.13.23': + resolution: {integrity: sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tanstack/virtual-core@3.13.23': + resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tremor/react@3.18.7': + resolution: {integrity: sha512-nmqvf/1m0GB4LXc7v2ftdfSLoZhy5WLrhV6HNf0SOriE6/l8WkYeWuhQq8QsBjRi94mUIKLJ/VC3/Y/pj6VubQ==} + peerDependencies: + react: ^18.0.0 + react-dom: '>=16.6.0' + '@turbo/darwin-64@2.9.6': resolution: {integrity: sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg==} cpu: [x64] @@ -810,12 +1264,21 @@ packages: cpu: [arm64] os: [win32] + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -827,6 +1290,39 @@ packages: '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.1': resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} @@ -839,6 +1335,10 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/marked@6.0.0': + resolution: {integrity: sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==} + deprecated: This is a stub types definition. marked provides its own type definitions, so you do not need this installed. + '@types/node@22.19.17': resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} @@ -865,6 +1365,57 @@ packages: '@types/web-push@3.6.4': resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@vitest/coverage-v8@4.1.4': + resolution: {integrity: sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==} + peerDependencies: + '@vitest/browser': 4.1.4 + vitest: 4.1.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.4': + resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} + + '@vitest/mocker@4.1.4': + resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.4': + resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} + + '@vitest/runner@4.1.4': + resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} + + '@vitest/snapshot@4.1.4': + resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} + + '@vitest/spy@4.1.4': + resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} + + '@vitest/utils@4.1.4': + resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -873,18 +1424,73 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.19: + resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==} + engines: {node: '>=6.0.0'} + hasBin: true + better-sqlite3@11.10.0: resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -898,6 +1504,15 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -919,15 +1534,35 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + caniuse-lite@1.0.30001788: resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -936,6 +1571,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-parser@1.4.7: resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} engines: {node: '>= 0.8.0'} @@ -954,13 +1592,76 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -978,6 +1679,12 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -990,6 +1697,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -998,6 +1709,21 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -1108,6 +1834,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.338: + resolution: {integrity: sha512-KVQQ3xko9/coDX3qXLUEEbqkKT8L+1DyAovrtu0Khtrt9wjSZ+7CZV4GVzxFy9Oe1NbrIU1oVXCwHJruIA1PNg==} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -1115,6 +1844,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1127,6 +1860,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1151,17 +1887,31 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express-session@1.19.0: resolution: {integrity: sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==} engines: {node: '>= 0.8.0'} @@ -1170,6 +1920,26 @@ packages: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -1177,6 +1947,10 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + finalhandler@1.3.2: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} @@ -1189,6 +1963,9 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -1196,6 +1973,11 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1223,10 +2005,22 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1235,6 +2029,13 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -1254,33 +2055,209 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isexe@3.1.5: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + jose@6.2.2: resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@29.0.2: + resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + marked@14.1.4: + resolution: {integrity: sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -1288,10 +2265,18 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1309,6 +2294,10 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -1324,6 +2313,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1375,6 +2367,13 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + oauth4webapi@3.8.5: resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} @@ -1382,10 +2381,17 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -1400,26 +2406,115 @@ packages: openid-client@6.8.3: resolution: {integrity: sha512-AoY/NaN9esS3+xvHInFSK0g3skSfeE0uqQAKRj4rB6/GsBIvzwTUaYo9+HcqpKIaP0dP85p5W07hayKgS4GAeA==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@0.1.13: resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -1427,10 +2522,17 @@ packages: pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qs@6.14.2: resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} engines: {node: '>=0.6'} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + random-bytes@1.0.0: resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} engines: {node: '>= 0.8'} @@ -1447,28 +2549,118 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-aria@3.48.0: + resolution: {integrity: sha512-jQjd4rBEIMqecBaAKYJbVGK6EqIHLa5znVQ7jwFyK5vCyljoj6KhgtiahmcIPsG5vG5vEDLw+ba+bEWn6A2P4w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-day-picker@8.10.1: + resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom@19.2.5: resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: react: ^19.2.5 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-stately@3.46.0: + resolution: {integrity: sha512-OdxhWvHgs2L4OJGIs7hnuTr5WjjMM6enhNEAMRqiekhF8+ITvA2LRwNftOZwcogaoCslGYq5S2VQTQwnm0GbCA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react-transition-state@2.3.3: + resolution: {integrity: sha512-wsIyg07ohlWEAYDZHvuXh/DY7mxlcLb0iqVv2aMXJ0gwgPVKNWKhOyNyzuJy/tt/6urSq0WT6BBZ/tdpybaAsQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react@19.2.5: resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1512,6 +2704,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -1529,13 +2724,23 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -1553,6 +2758,33 @@ packages: babel-plugin-macros: optional: true + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tailwind-merge@2.6.1: + resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -1560,10 +2792,57 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} + + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1595,10 +2874,25 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -1610,6 +2904,97 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.4: + resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.4 + '@vitest/browser-preview': 4.1.4 + '@vitest/browser-webdriverio': 4.1.4 + '@vitest/coverage-istanbul': 4.1.4 + '@vitest/coverage-v8': 4.1.4 + '@vitest/ui': 4.1.4 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + web-push@3.6.7: resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} engines: {node: '>= 16'} @@ -1619,26 +3004,132 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which@4.0.0: resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} snapshots: + '@adobe/css-tools@4.4.4': {} + + '@alloc/quick-lru@5.2.0': {} + + '@asamuzakjp/css-color@5.1.10': + dependencies: + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.0.9': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.29.2': {} + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@drizzle-team/brocli@0.10.2': {} + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -1862,6 +3353,56 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true + '@exodus/bytes@1.15.0': {} + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@1.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@floating-ui/react@0.19.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/react-dom': 1.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + tabbable: 6.4.0 + + '@floating-ui/react@0.26.28(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@floating-ui/utils': 0.2.11 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + tabbable: 6.4.0 + + '@floating-ui/utils@0.2.11': {} + + '@headlessui/react@2.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@react-aria/focus': 3.22.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@react-aria/interactions': 3.28.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-virtual': 3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + '@img/colour@1.1.0': optional: true @@ -1959,6 +3500,39 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@internationalized/date@3.12.1': + dependencies: + '@swc/helpers': 0.5.15 + + '@internationalized/number@3.6.6': + dependencies: + '@swc/helpers': 0.5.15 + + '@internationalized/string@3.2.8': + dependencies: + '@swc/helpers': 0.5.15 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + '@next/env@15.5.15': {} '@next/swc-darwin-arm64@15.5.15': @@ -1985,12 +3559,166 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.15': optional: true + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-project/types@0.124.0': {} + '@petamoriken/float16@3.9.3': {} + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + + '@react-aria/focus@3.22.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@swc/helpers': 0.5.15 + react: 19.2.5 + react-aria: 3.48.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react-dom: 19.2.5(react@19.2.5) + + '@react-aria/interactions@3.28.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@react-types/shared': 3.34.0(react@19.2.5) + '@swc/helpers': 0.5.15 + react: 19.2.5 + react-aria: 3.48.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react-dom: 19.2.5(react@19.2.5) + + '@react-types/shared@3.34.0(react@19.2.5)': + dependencies: + react: 19.2.5 + + '@rolldown/binding-android-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.15': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@standard-schema/spec@1.1.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 + '@tanstack/react-table@8.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@tanstack/react-virtual@3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/virtual-core': 3.13.23 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@tanstack/table-core@8.21.3': {} + + '@tanstack/virtual-core@3.13.23': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 10.4.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@tremor/react@3.18.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/react': 0.19.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@headlessui/react': 2.2.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + date-fns: 3.6.0 + react: 19.2.5 + react-day-picker: 8.10.1(date-fns@3.6.0)(react@19.2.5) + react-dom: 19.2.5(react@19.2.5) + react-transition-state: 2.3.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + recharts: 2.15.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + tailwind-merge: 2.6.1 + '@turbo/darwin-64@2.9.6': optional: true @@ -2009,6 +3737,13 @@ snapshots: '@turbo/windows-arm64@2.9.6': optional: true + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + '@types/better-sqlite3@7.6.13': dependencies: '@types/node': 22.19.17 @@ -2018,6 +3753,11 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.19.17 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/connect@3.4.38': dependencies: '@types/node': 22.19.17 @@ -2030,6 +3770,34 @@ snapshots: dependencies: '@types/node': 22.19.17 + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.1': dependencies: '@types/node': 22.19.17 @@ -2049,6 +3817,10 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/marked@6.0.0': + dependencies: + marked: 14.1.4 + '@types/node@22.19.17': dependencies: undici-types: 6.21.0 @@ -2078,6 +3850,74 @@ snapshots: dependencies: '@types/node': 22.19.17 + '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0) + + '@vitest/coverage-v8@4.1.4(vitest@4.1.4)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.4 + ast-v8-to-istanbul: 1.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)) + + '@vitest/expect@4.1.4': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0) + + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0) + + '@vitest/pretty-format@4.1.4': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.4': + dependencies: + '@vitest/utils': 4.1.4 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + '@vitest/utils': 4.1.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.4': {} + + '@vitest/utils@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -2085,6 +3925,29 @@ snapshots: agent-base@7.1.4: {} + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + arg@5.0.2: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + array-flatten@1.1.1: {} asn1.js@5.4.1: @@ -2094,13 +3957,38 @@ snapshots: minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + autoprefixer@10.5.0(postcss@8.5.9): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001788 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.9 + postcss-value-parser: 4.2.0 + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.19: {} + better-sqlite3@11.10.0: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + binary-extensions@2.3.0: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -2130,6 +4018,18 @@ snapshots: transitivePeerDependencies: - supports-color + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.19 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.338 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -2151,18 +4051,40 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + camelcase-css@2.0.1: {} + caniuse-lite@1.0.30001788: {} + chai@6.2.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chownr@1.1.4: {} client-only@0.0.1: {} + clsx@2.1.1: {} + + commander@4.1.1: {} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-parser@1.4.7: dependencies: cookie: 0.7.2 @@ -2179,10 +4101,66 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + cssesc@3.0.0: {} + csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + data-uri-to-buffer@4.0.1: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + date-fns@3.6.0: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -2191,6 +4169,10 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + + decimal.js@10.6.0: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -2199,10 +4181,25 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-libc@2.1.2: {} + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.2 + csstype: 3.2.3 + dotenv@16.6.1: {} drizzle-kit@0.30.6: @@ -2234,18 +4231,24 @@ snapshots: ee-first@1.1.1: {} + electron-to-chromium@1.5.338: {} + encodeurl@2.0.0: {} end-of-stream@1.4.5: dependencies: once: 1.4.0 + entities@6.0.1: {} + env-paths@3.0.0: {} es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -2337,12 +4340,22 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} + escape-html@1.0.3: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + etag@1.8.1: {} + eventemitter3@4.0.7: {} + expand-template@2.0.3: {} + expect-type@1.3.0: {} + express-session@1.19.0: dependencies: cookie: 0.7.2 @@ -2392,6 +4405,24 @@ snapshots: transitivePeerDependencies: - supports-color + fast-equals@5.4.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -2399,6 +4430,10 @@ snapshots: file-uri-to-path@1.0.0: {} + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + finalhandler@1.3.2: dependencies: debug: 2.6.9 @@ -2417,10 +4452,15 @@ snapshots: forwarded@0.2.0: {} + fraction.js@5.3.4: {} + fresh@0.5.2: {} fs-constants@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -2461,14 +4501,32 @@ snapshots: github-from-package@0.0.0: {} + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + gopd@1.2.0: {} + has-flag@4.0.0: {} + has-symbols@1.1.0: {} hasown@2.0.2: dependencies: function-bind: 1.1.2 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + + html-escaper@2.0.2: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -2492,16 +4550,83 @@ snapshots: ieee754@1.2.1: {} + indent-string@4.0.0: {} + inherits@2.0.4: {} ini@1.3.8: {} + internmap@2.0.3: {} + ipaddr.js@1.9.1: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + isexe@3.1.5: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jiti@1.21.7: {} + jose@6.2.2: {} + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + jsdom@29.0.2: + dependencies: + '@asamuzakjp/css-color': 5.1.10 + '@asamuzakjp/dom-selector': 7.0.9 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.5 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -2513,14 +4638,102 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lodash@4.18.1: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@11.3.5: {} + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + marked@14.1.4: {} + math-intrinsics@1.1.0: {} + mdn-data@2.27.1: {} + media-typer@0.3.0: {} merge-descriptors@1.0.3: {} + merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mime-db@1.52.0: {} mime-types@2.1.35: @@ -2531,6 +4744,8 @@ snapshots: mimic-response@3.1.0: {} + min-indent@1.0.1: {} + minimalistic-assert@1.0.1: {} minimist@1.2.8: {} @@ -2541,6 +4756,12 @@ snapshots: ms@2.1.3: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.11: {} nanoid@5.1.7: {} @@ -2549,7 +4770,7 @@ snapshots: negotiator@0.6.3: {} - next@15.5.15(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next@15.5.15(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 15.5.15 '@swc/helpers': 0.5.15 @@ -2567,6 +4788,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.15 '@next/swc-win32-arm64-msvc': 15.5.15 '@next/swc-win32-x64-msvc': 15.5.15 + '@playwright/test': 1.59.1 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -2584,12 +4806,20 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-releases@2.0.37: {} + + normalize-path@3.0.0: {} + oauth4webapi@3.8.5: {} object-assign@4.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} + obug@2.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -2605,18 +4835,80 @@ snapshots: jose: 6.2.2 oauth4webapi: 3.8.5 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} + path-parse@1.0.7: {} + path-to-regexp@0.1.13: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss-import@15.1.0(postcss@8.5.9): + dependencies: + postcss: 8.5.9 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.12 + + postcss-js@4.1.0(postcss@8.5.9): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.9 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.9)(tsx@4.21.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.9 + tsx: 4.21.0 + + postcss-nested@6.2.0(postcss@8.5.9): + dependencies: + postcss: 8.5.9 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + postcss@8.4.31: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.9: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -2632,6 +4924,18 @@ snapshots: tar-fs: 2.1.4 tunnel-agent: 0.6.0 + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -2642,10 +4946,14 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + punycode@2.3.1: {} + qs@6.14.2: dependencies: side-channel: 1.1.0 + queue-microtask@1.2.3: {} + random-bytes@1.0.0: {} range-parser@1.2.1: {} @@ -2664,25 +4972,152 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-aria@3.48.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + '@internationalized/date': 3.12.1 + '@internationalized/number': 3.6.6 + '@internationalized/string': 3.2.8 + '@react-types/shared': 3.34.0(react@19.2.5) + '@swc/helpers': 0.5.15 + aria-hidden: 1.2.6 + clsx: 2.1.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-stately: 3.46.0(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) + + react-day-picker@8.10.1(date-fns@3.6.0)(react@19.2.5): + dependencies: + date-fns: 3.6.0 + react: 19.2.5 + react-dom@19.2.5(react@19.2.5): dependencies: react: 19.2.5 scheduler: 0.27.0 + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-smooth@4.0.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + fast-equals: 5.4.0 + prop-types: 15.8.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-transition-group: 4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + + react-stately@3.46.0(react@19.2.5): + dependencies: + '@internationalized/date': 3.12.1 + '@internationalized/number': 3.6.6 + '@internationalized/string': 3.2.8 + '@react-types/shared': 3.34.0(react@19.2.5) + '@swc/helpers': 0.5.15 + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) + + react-transition-group@4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + '@babel/runtime': 7.29.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + react-transition-state@2.3.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react@19.2.5: {} + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.18.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rolldown@1.0.0-rc.15: + dependencies: + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} semver@7.7.4: {} @@ -2778,6 +5213,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + simple-concat@1.0.1: {} simple-get@4.0.1: @@ -2795,12 +5232,20 @@ snapshots: source-map@0.6.1: {} + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@4.1.0: {} + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@2.0.1: {} styled-jsx@5.1.6(react@19.2.5): @@ -2808,6 +5253,56 @@ snapshots: client-only: 0.0.1 react: 19.2.5 + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.16 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + tabbable@6.4.0: {} + + tailwind-merge@2.6.1: {} + + tailwindcss@3.4.19(tsx@4.21.0): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.9 + postcss-import: 15.1.0(postcss@8.5.9) + postcss-js: 4.1.0(postcss@8.5.9) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.9)(tsx@4.21.0) + postcss-nested: 6.2.0(postcss@8.5.9) + postcss-selector-parser: 6.1.2 + resolve: 1.22.12 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -2823,8 +5318,49 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.1.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tldts-core@7.0.28: {} + + tldts@7.0.28: + dependencies: + tldts-core: 7.0.28 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + toidentifier@1.0.1: {} + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.28 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + ts-interface-checker@0.1.13: {} + tslib@2.8.1: {} tsx@4.21.0: @@ -2860,14 +5396,133 @@ snapshots: undici-types@6.21.0: {} + undici@7.25.0: {} + unpipe@1.0.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@19.2.5): + dependencies: + react: 19.2.5 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} vary@1.1.2: {} + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.9 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 22.19.17 + esbuild: 0.19.12 + fsevents: 2.3.3 + jiti: 1.21.7 + tsx: 4.21.0 + + vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.9 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 22.19.17 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 1.21.7 + tsx: 4.21.0 + + vitest@4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.17 + '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) + jsdom: 29.0.2 + transitivePeerDependencies: + - msw + + vitest@4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.17 + '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) + jsdom: 29.0.2 + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + web-push@3.6.7: dependencies: asn1.js: 5.4.1 @@ -2880,10 +5535,31 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which@4.0.0: dependencies: isexe: 3.1.5 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrappy@1.0.2: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + zod@3.25.76: {} diff --git a/services/api/package.json b/services/api/package.json index 608efae..2a1a1f0 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -8,6 +8,9 @@ "build": "tsc", "dev": "tsx watch src/index.ts", "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "type-check": "tsc --noEmit", "clean": "rm -rf dist" }, @@ -33,8 +36,10 @@ "@types/express": "^5.0.0", "@types/express-session": "^1.18.1", "@types/web-push": "^3.6.4", + "@vitest/coverage-v8": "^4.1.4", "drizzle-kit": "^0.30.4", "tsx": "^4.19.2", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "vitest": "^4.1.4" } } diff --git a/services/api/src/db/index.ts b/services/api/src/db/index.ts index 9f04763..8422a81 100644 --- a/services/api/src/db/index.ts +++ b/services/api/src/db/index.ts @@ -99,6 +99,40 @@ export function runMigrations() { sql TEXT NOT NULL, created_at TEXT NOT NULL ); + + CREATE TABLE IF NOT EXISTS sim_runs ( + id TEXT PRIMARY KEY, + policy_a TEXT NOT NULL, + policy_b TEXT NOT NULL, + n_users INTEGER NOT NULL, + n_rounds INTEGER NOT NULL, + tasks_per_round INTEGER NOT NULL DEFAULT 8, + use_llm INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'pending', + summary_json TEXT, + winner TEXT, + persona_breakdown_json TEXT, + created_at TEXT NOT NULL, + finished_at TEXT + ); + + CREATE TABLE IF NOT EXISTS sim_events ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL REFERENCES sim_runs(id), + round INTEGER NOT NULL, + user_id TEXT NOT NULL, + persona TEXT NOT NULL, + policy TEXT NOT NULL, + tip_content TEXT NOT NULL, + priority INTEGER NOT NULL, + is_overdue INTEGER NOT NULL, + action TEXT NOT NULL, + dwell_ms INTEGER, + reward_milli INTEGER NOT NULL, + hour INTEGER NOT NULL, + day_of_week INTEGER NOT NULL, + created_at TEXT NOT NULL + ); `); // Additive column migrations — safe to run on existing DBs. @@ -106,6 +140,8 @@ export function runMigrations() { for (const stmt of [ `ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'`, `ALTER TABLE push_subscriptions ADD COLUMN created_at TEXT NOT NULL DEFAULT ''`, + `ALTER TABLE tip_feedback ADD COLUMN dwell_ms INTEGER`, + `ALTER TABLE tip_feedback ADD COLUMN reward_milli INTEGER`, ]) { try { sqlite.exec(stmt); } catch { /* column already exists */ } } diff --git a/services/api/src/db/schema.ts b/services/api/src/db/schema.ts index c8d5c3b..b38a8c7 100644 --- a/services/api/src/db/schema.ts +++ b/services/api/src/db/schema.ts @@ -29,6 +29,8 @@ export const tipFeedback = sqliteTable('tip_feedback', { tipId: text('tip_id').notNull(), action: text('action').notNull(), // 'done' | 'dismiss' | 'snooze' sourceId: text('source_id'), + dwellMs: integer('dwell_ms'), // ms between servedAt and feedback; null if unknown + rewardMilli: integer('reward_milli'), // inferred reward × 1000 (e.g. 1000 = +1.0) createdAt: text('created_at').notNull(), }); @@ -81,6 +83,43 @@ export const tipScores = sqliteTable('tip_scores', { servedAt: text('served_at').notNull(), }); +// ── Simulation runs ────────────────────────────────────────────────────────── +// One row per offline simulation run (two-policy comparison). +export const simRuns = sqliteTable('sim_runs', { + id: text('id').primaryKey(), + policyA: text('policy_a').notNull(), + policyB: text('policy_b').notNull(), + nUsers: integer('n_users').notNull(), + nRounds: integer('n_rounds').notNull(), + tasksPerRound: integer('tasks_per_round').notNull().default(8), + useLlm: integer('use_llm', { mode: 'boolean' }).notNull().default(false), + status: text('status').notNull().default('pending'), // 'pending'|'running'|'done'|'failed' + summaryJson: text('summary_json'), // JSON: { [policy]: PolicySummary } + winner: text('winner'), + personaBreakdownJson: text('persona_breakdown_json'), // JSON: { [persona]: { [policy]: {reward,n} } } + createdAt: text('created_at').notNull(), + finishedAt: text('finished_at'), +}); + +// One row per tip served in a simulation round. +export const simEvents = sqliteTable('sim_events', { + id: text('id').primaryKey(), + runId: text('run_id').notNull().references(() => simRuns.id), + round: integer('round').notNull(), + userId: text('user_id').notNull(), + persona: text('persona').notNull(), + policy: text('policy').notNull(), + tipContent: text('tip_content').notNull(), + priority: integer('priority').notNull(), + isOverdue: integer('is_overdue', { mode: 'boolean' }).notNull(), + action: text('action').notNull(), // 'done' | 'snooze' | 'dismiss' + dwellMs: integer('dwell_ms'), // simulated ms between tip appear and user action + rewardMilli: integer('reward_milli').notNull(), // inferred reward × 1000 + hour: integer('hour').notNull(), + dayOfWeek: integer('day_of_week').notNull(), + createdAt: text('created_at').notNull(), +}); + // Admin saved SQL queries. export const savedQueries = sqliteTable('saved_queries', { id: text('id').primaryKey(), diff --git a/services/api/src/events/__tests__/bus.test.ts b/services/api/src/events/__tests__/bus.test.ts new file mode 100644 index 0000000..82528d6 --- /dev/null +++ b/services/api/src/events/__tests__/bus.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect, vi } from 'vitest'; +import { Bus, bus } from '../bus.js'; + +// Use a fresh Bus instance for isolation in most tests +function makeBus() { + return new Bus(); +} + +describe('EventBus — delivery', () => { + it('delivers a published event to subscribers', () => { + const b = makeBus(); + const handler = vi.fn(); + b.subscribe('signals.tip.served', handler); + + const payload = { userId: 'u1', tipId: 'tip:1', policy: 'random', servedAt: new Date().toISOString() }; + b.publish('signals.tip.served', payload); + + expect(handler).toHaveBeenCalledOnce(); + expect(handler).toHaveBeenCalledWith(payload); + }); + + it('delivers to multiple subscribers on the same subject', () => { + const b = makeBus(); + const h1 = vi.fn(); + const h2 = vi.fn(); + b.subscribe('signals.tip.served', h1); + b.subscribe('signals.tip.served', h2); + + b.publish('signals.tip.served', { userId: 'u', tipId: 't', policy: 'p', servedAt: '' }); + + expect(h1).toHaveBeenCalledOnce(); + expect(h2).toHaveBeenCalledOnce(); + }); + + it('does not deliver to handlers on a different subject', () => { + const b = makeBus(); + const feedbackHandler = vi.fn(); + b.subscribe('signals.tip.feedback', feedbackHandler); + + b.publish('signals.tip.served', { userId: 'u', tipId: 't', policy: 'p', servedAt: '' }); + + expect(feedbackHandler).not.toHaveBeenCalled(); + }); + + it('does not call a handler after bus.off()', () => { + const b = makeBus(); + const handler = vi.fn(); + b.subscribe('signals.tip.served', handler); + b.off('signals.tip.served', handler); + + b.publish('signals.tip.served', { userId: 'u', tipId: 't', policy: 'p', servedAt: '' }); + + expect(handler).not.toHaveBeenCalled(); + }); + + it('does not throw when publishing with no subscribers', () => { + const b = makeBus(); + expect(() => + b.publish('signals.task.synced', { userId: 'u', count: 3, syncedAt: '' }), + ).not.toThrow(); + }); + + it('reward maps correctly: done=1, dismiss=-1, snooze=0', () => { + const b = makeBus(); + const cases: Array<['done' | 'dismiss' | 'snooze', number]> = [ + ['done', 1.0], + ['dismiss', -1.0], + ['snooze', 0.0], + ]; + + for (const [action, expected] of cases) { + const handler = vi.fn(); + b.subscribe('signals.tip.feedback', handler); + + const payload = { + userId: 'u1', + tipId: 'todoist:42', + action, + reward: action === 'done' ? 1.0 : action === 'dismiss' ? -1.0 : 0.0, + dwellMs: null, + createdAt: new Date().toISOString(), + }; + b.publish('signals.tip.feedback', payload); + + expect(handler).toHaveBeenCalledWith(expect.objectContaining({ reward: expected })); + b.off('signals.tip.feedback', handler); + } + }); +}); + +describe('EventBus — ring buffer / tail()', () => { + it('tail() returns published events', () => { + const b = makeBus(); + b.publish('signals.tip.served', { userId: 'u1', tipId: 't1', policy: 'p', servedAt: '' }); + b.publish('signals.tip.served', { userId: 'u2', tipId: 't2', policy: 'p', servedAt: '' }); + + const events = b.tail(); + expect(events.length).toBeGreaterThanOrEqual(2); + }); + + it('tail() filters by subject prefix', () => { + const b = makeBus(); + b.publish('signals.tip.served', { userId: 'u', tipId: 't', policy: 'p', servedAt: '' }); + b.publish('signals.task.synced', { userId: 'u', count: 1, syncedAt: '' }); + + const tipEvents = b.tail({ subject: 'signals.tip' }); + expect(tipEvents.every((e) => e.subject.startsWith('signals.tip'))).toBe(true); + + const taskEvents = b.tail({ subject: 'signals.task' }); + expect(taskEvents.every((e) => e.subject.startsWith('signals.task'))).toBe(true); + }); + + it('tail() filters by userId', () => { + const b = makeBus(); + b.publish('signals.tip.served', { userId: 'alice', tipId: 't1', policy: 'p', servedAt: '' }); + b.publish('signals.tip.served', { userId: 'bob', tipId: 't2', policy: 'p', servedAt: '' }); + + const aliceEvents = b.tail({ userId: 'alice' }); + expect(aliceEvents.every((e) => (e.payload as any).userId === 'alice')).toBe(true); + }); + + it('tail() respects limit', () => { + const b = makeBus(); + for (let i = 0; i < 10; i++) { + b.publish('signals.tip.served', { userId: 'u', tipId: `t${i}`, policy: 'p', servedAt: '' }); + } + const events = b.tail({ limit: 3 }); + expect(events).toHaveLength(3); + }); + + it('tail() returns only events after `since` id', () => { + const b = makeBus(); + b.publish('signals.tip.served', { userId: 'u', tipId: 't1', policy: 'p', servedAt: '' }); + const snap = b.tail(); + const lastId = snap[snap.length - 1].id; + + b.publish('signals.tip.served', { userId: 'u', tipId: 't2', policy: 'p', servedAt: '' }); + + const after = b.tail({ since: lastId }); + expect(after).toHaveLength(1); + expect((after[0].payload as any).tipId).toBe('t2'); + }); + + it('assigns monotonically increasing ids', () => { + const b = makeBus(); + b.publish('signals.tip.served', { userId: 'u', tipId: 't1', policy: 'p', servedAt: '' }); + b.publish('signals.tip.served', { userId: 'u', tipId: 't2', policy: 'p', servedAt: '' }); + + const events = b.tail(); + const ids = events.map((e) => e.id); + for (let i = 1; i < ids.length; i++) { + expect(ids[i]).toBeGreaterThan(ids[i - 1]); + } + }); + + it('ring buffer caps at 500 entries and evicts oldest', () => { + const b = makeBus(); + // Publish 502 events — the first two should be evicted + for (let i = 0; i < 502; i++) { + b.publish('signals.tip.served', { userId: 'u', tipId: `t${i}`, policy: 'p', servedAt: '' }); + } + const all = b.tail({ limit: 1000 }); + expect(all).toHaveLength(500); + // Oldest surviving entry should be the 3rd published (index 2) + expect((all[0].payload as any).tipId).toBe('t2'); + }); +}); + +describe('EventBus — singleton bus export', () => { + it('singleton bus is a Bus instance', () => { + expect(bus).toBeInstanceOf(Bus); + }); +}); diff --git a/services/api/src/events/bus.ts b/services/api/src/events/bus.ts index fd3a0b9..de47d91 100644 --- a/services/api/src/events/bus.ts +++ b/services/api/src/events/bus.ts @@ -22,8 +22,9 @@ export type TipServedEvent = { export type TipFeedbackEvent = { userId: string; tipId: string; - action: 'done' | 'dismiss' | 'snooze' | 'helpful' | 'not_helpful'; - reward: number; + action: 'done' | 'dismiss' | 'snooze'; + reward: number; // inferred from action + dwellMs (see inferReward in recommender.ts) + dwellMs: number | null; createdAt: string; }; @@ -91,4 +92,5 @@ class Bus extends EventEmitter { } } +export { Bus }; export const bus = new Bus(); diff --git a/services/api/src/middleware/__tests__/admin.test.ts b/services/api/src/middleware/__tests__/admin.test.ts new file mode 100644 index 0000000..88e4b8f --- /dev/null +++ b/services/api/src/middleware/__tests__/admin.test.ts @@ -0,0 +1,109 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { Response, NextFunction } from 'express'; +import type { AuthenticatedRequest } from '../session.js'; + +// Mock the db module so requireAdmin uses our test db +const mockSelect = vi.fn(); +vi.mock('../../db/index.js', () => ({ db: { select: mockSelect } })); + +// Import AFTER mock is set up +const { requireAdmin } = await import('../admin.js'); + +function makeRes() { + const json = vi.fn(); + const status = vi.fn().mockReturnValue({ json }); + return { status, json, _status: status, _json: json } as unknown as Response & { + _status: ReturnType; + _json: ReturnType; + }; +} + +function makeReq(userId?: string): AuthenticatedRequest { + return { userId } as AuthenticatedRequest; +} + +describe('requireAdmin middleware', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('calls next() when user has role=admin', async () => { + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([{ role: 'admin' }]), + }), + }), + }); + + const next: NextFunction = vi.fn() as unknown as NextFunction; + await requireAdmin(makeReq('user-1'), makeRes(), next); + expect(next).toHaveBeenCalledOnce(); + }); + + it('returns 403 when user has role=user', async () => { + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([{ role: 'user' }]), + }), + }), + }); + + const res = makeRes(); + const next: NextFunction = vi.fn() as unknown as NextFunction; + await requireAdmin(makeReq('user-2'), res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 403 when user is not found', async () => { + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + }), + }), + }); + + const res = makeRes(); + const next: NextFunction = vi.fn() as unknown as NextFunction; + await requireAdmin(makeReq('unknown'), res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 403 when userId is undefined (unauthenticated request)', async () => { + // DB will return empty — userId is undefined so the query matches nothing + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + }), + }), + }); + + const res = makeRes(); + const next: NextFunction = vi.fn() as unknown as NextFunction; + await requireAdmin(makeReq(undefined), res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(next).not.toHaveBeenCalled(); + }); + + it('propagates DB errors (does not swallow exceptions)', async () => { + mockSelect.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockRejectedValue(new Error('DB down')), + }), + }), + }); + + const res = makeRes(); + const next: NextFunction = vi.fn() as unknown as NextFunction; + await expect(requireAdmin(makeReq('user-1'), res, next)).rejects.toThrow('DB down'); + }); +}); diff --git a/services/api/src/middleware/admin.ts b/services/api/src/middleware/admin.ts new file mode 100644 index 0000000..0620d08 --- /dev/null +++ b/services/api/src/middleware/admin.ts @@ -0,0 +1,27 @@ +import { Response, NextFunction } from 'express'; +import { db } from '../db/index.js'; +import { users } from '../db/schema.js'; +import { eq } from 'drizzle-orm'; +import { AuthenticatedRequest } from './session.js'; + +/** + * Requires the session user to have role='admin'. + * Must be used after requireAuth (which sets req.userId). + */ +export async function requireAdmin( + req: AuthenticatedRequest, + res: Response, + next: NextFunction, +) { + const [user] = await db + .select({ role: users.role }) + .from(users) + .where(eq(users.id, req.userId!)) + .limit(1); + + if (!user || user.role !== 'admin') { + res.status(403).json({ error: 'Forbidden' }); + return; + } + next(); +} diff --git a/services/api/src/routes/__tests__/admin.test.ts b/services/api/src/routes/__tests__/admin.test.ts new file mode 100644 index 0000000..9b34c2f --- /dev/null +++ b/services/api/src/routes/__tests__/admin.test.ts @@ -0,0 +1,370 @@ +/** + * Admin route integration tests. + * + * A real Express app + in-memory SQLite DB per test suite. + * Auth and admin middleware are mocked so we can focus on route logic. + */ +import { describe, it, expect, vi, beforeAll } from 'vitest'; +import express from 'express'; +import * as http from 'http'; +import { makeTestDb } from '../../test/db.js'; +import { users, integrationTokens, tipViews, tipFeedback } from '../../db/schema.js'; + +// ---- in-memory DB ---- +const testDb = makeTestDb(); + +vi.mock('../../db/index.js', () => ({ db: testDb })); + +// Bypass auth — all requests arrive pre-authenticated as 'admin-1' +vi.mock('../../middleware/session.js', () => ({ + sessionMiddleware: (_req: express.Request, _res: express.Response, next: express.NextFunction) => + next(), + requireAuth: (req: express.Request, _res: express.Response, next: express.NextFunction) => { + (req as any).userId = 'admin-1'; + next(); + }, +})); + +vi.mock('../../middleware/admin.js', () => ({ + requireAdmin: (_req: express.Request, _res: express.Response, next: express.NextFunction) => + next(), +})); + +const { adminRouter } = await import('../admin.js'); + +// ---- seed ---- +const NOW = new Date().toISOString(); +const DAY_AGO = new Date(Date.now() - 23 * 60 * 60 * 1000).toISOString(); + +beforeAll(async () => { + await testDb.insert(users).values([ + { id: 'admin-1', email: 'admin@test.com', role: 'admin', consentGiven: true, consentAt: NOW, createdAt: NOW }, + { id: 'user-1', email: 'alice@test.com', role: 'user', consentGiven: true, consentAt: NOW, createdAt: NOW }, + { id: 'user-2', email: 'bob@test.com', role: 'user', consentGiven: false, createdAt: NOW }, + ]); + await testDb.insert(integrationTokens).values([ + { id: 'tok-1', userId: 'user-1', provider: 'todoist', accessToken: 'secret', connectedAt: NOW }, + ]); + await testDb.insert(tipViews).values([ + { id: 'tv-1', userId: 'user-1', tipId: 'tip:a', servedAt: DAY_AGO }, + { id: 'tv-2', userId: 'user-1', tipId: 'tip:b', servedAt: NOW }, + { id: 'tv-3', userId: 'user-2', tipId: 'tip:c', servedAt: NOW }, + ]); + await testDb.insert(tipFeedback).values([ + { id: 'tf-1', userId: 'user-1', tipId: 'tip:a', action: 'done', createdAt: DAY_AGO }, + { id: 'tf-2', userId: 'user-1', tipId: 'tip:b', action: 'snooze', createdAt: NOW }, + ]); +}); + +// ---- test helpers ---- +function buildApp() { + const app = express(); + app.use(express.json()); + app.use('/api/admin', adminRouter); + return app; +} + +function call( + server: http.Server, + method: string, + path: string, + body?: unknown, +): Promise<{ status: number; body: unknown }> { + return new Promise((resolve, reject) => { + const { port } = server.address() as { port: number }; + const req = http.request( + { method, hostname: '127.0.0.1', port, path, headers: { 'Content-Type': 'application/json' } }, + (res) => { + let data = ''; + res.on('data', (c) => (data += c)); + res.on('end', () => { + try { resolve({ status: res.statusCode!, body: JSON.parse(data) }); } + catch { resolve({ status: res.statusCode!, body: data }); } + }); + }, + ); + req.on('error', reject); + if (body) req.write(JSON.stringify(body)); + req.end(); + }); +} + +function startServer(app: express.Application): Promise<{ server: http.Server; call: (method: string, path: string, body?: unknown) => ReturnType }> { + return new Promise((resolve) => { + const server = http.createServer(app); + server.listen(0, () => + resolve({ server, call: (m, p, b) => call(server, m, p, b) }), + ); + }); +} + +// ---- tests ---- +describe('GET /api/admin/stats', () => { + it('returns dau, wau, tips, reactions, user totals', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status, body } = await call('GET', '/api/admin/stats'); + const b = body as Record; + expect(status).toBe(200); + expect(typeof b.dau).toBe('number'); + expect(typeof b.wau).toBe('number'); + expect(b.tipsServedLast7d).toBeGreaterThanOrEqual(3); + expect(b.totalUsers).toBe(3); + expect(b.activatedUsers).toBeGreaterThanOrEqual(2); + expect(b.reactionsLast7d).toBeDefined(); + } finally { + server.close(); + } + }); +}); + +describe('GET /api/admin/users', () => { + it('returns paginated list with total', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status, body } = await call('GET', '/api/admin/users?limit=10&offset=0'); + const b = body as { users: unknown[]; total: number }; + expect(status).toBe(200); + expect(b.total).toBe(3); + expect(b.users).toHaveLength(3); + } finally { + server.close(); + } + }); + + it('respects limit', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status, body } = await call('GET', '/api/admin/users?limit=2&offset=0'); + const b = body as { users: unknown[]; total: number }; + expect(status).toBe(200); + expect(b.users).toHaveLength(2); + expect(b.total).toBe(3); + } finally { + server.close(); + } + }); +}); + +describe('GET /api/admin/users/:id', () => { + it('returns user detail with integrations and tip stats', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status, body } = await call('GET', '/api/admin/users/user-1'); + const b = body as { + user: { email: string; role: string }; + integrations: { provider: string }[]; + tipsServed: number; + recentFeedback: unknown[]; + }; + expect(status).toBe(200); + expect(b.user.email).toBe('alice@test.com'); + expect(b.user.role).toBe('user'); + expect(b.integrations).toHaveLength(1); + expect(b.integrations[0].provider).toBe('todoist'); + expect(b.tipsServed).toBe(2); + expect(b.recentFeedback).toHaveLength(2); + } finally { + server.close(); + } + }); + + it('returns 404 for unknown user', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status } = await call('GET', '/api/admin/users/nonexistent'); + expect(status).toBe(404); + } finally { + server.close(); + } + }); +}); + +describe('GET /api/admin/audit', () => { + it('returns list and total', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status, body } = await call('GET', '/api/admin/audit'); + const b = body as { actions: unknown[]; total: number }; + expect(status).toBe(200); + expect(Array.isArray(b.actions)).toBe(true); + expect(typeof b.total).toBe('number'); + } finally { + server.close(); + } + }); +}); + +describe('POST /api/admin/users/:id/revoke-integration', () => { + it('removes the integration and writes an audit entry', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status, body } = await call( + 'POST', '/api/admin/users/user-1/revoke-integration', { provider: 'todoist' }, + ); + expect(status).toBe(200); + expect((body as { ok: boolean }).ok).toBe(true); + + // Integration should be gone + const detail = await call('GET', '/api/admin/users/user-1'); + expect((detail.body as { integrations: unknown[] }).integrations).toHaveLength(0); + + // Audit log should contain the action + const audit = await call('GET', '/api/admin/audit'); + const actions = (audit.body as { actions: { action: string }[] }).actions; + expect(actions.some((x) => x.action === 'revoke_integration')).toBe(true); + } finally { + server.close(); + } + }); + + it('returns 404 for non-existent integration', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status } = await call( + 'POST', '/api/admin/users/user-2/revoke-integration', { provider: 'todoist' }, + ); + expect(status).toBe(404); + } finally { + server.close(); + } + }); + + it('returns 400 when provider is missing', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status } = await call('POST', '/api/admin/users/user-1/revoke-integration', {}); + expect(status).toBe(400); + } finally { + server.close(); + } + }); + + it('returns 404 when target user does not exist', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status } = await call( + 'POST', '/api/admin/users/ghost/revoke-integration', { provider: 'todoist' }, + ); + expect(status).toBe(404); + } finally { + server.close(); + } + }); +}); + +describe('GET /api/admin/users — pagination', () => { + it('offset skips rows', async () => { + const { server, call } = await startServer(buildApp()); + try { + const page0 = await call('GET', '/api/admin/users?limit=2&offset=0'); + const page1 = await call('GET', '/api/admin/users?limit=2&offset=2'); + const b0 = page0.body as { users: { id: string }[]; total: number }; + const b1 = page1.body as { users: { id: string }[]; total: number }; + expect(b0.users).toHaveLength(2); + expect(b1.users).toHaveLength(1); + // total stays constant + expect(b0.total).toBe(3); + expect(b1.total).toBe(3); + // no overlap + const ids0 = b0.users.map((u) => u.id); + const ids1 = b1.users.map((u) => u.id); + expect(ids0.every((id) => !ids1.includes(id))).toBe(true); + } finally { + server.close(); + } + }); + + it('offset beyond total returns empty users array', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status, body } = await call('GET', '/api/admin/users?limit=10&offset=999'); + const b = body as { users: unknown[]; total: number }; + expect(status).toBe(200); + expect(b.users).toHaveLength(0); + expect(b.total).toBe(3); + } finally { + server.close(); + } + }); + + it('limit is capped at 200', async () => { + const { server, call } = await startServer(buildApp()); + try { + // Passing a huge limit should not crash and should return at most all users + const { status, body } = await call('GET', '/api/admin/users?limit=9999'); + const b = body as { users: unknown[] }; + expect(status).toBe(200); + expect(b.users.length).toBeLessThanOrEqual(200); + } finally { + server.close(); + } + }); +}); + +describe('GET /api/admin/events', () => { + it('returns events array and nextSince', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status, body } = await call('GET', '/api/admin/events'); + const b = body as { events: unknown[]; nextSince: number }; + expect(status).toBe(200); + expect(Array.isArray(b.events)).toBe(true); + expect(typeof b.nextSince).toBe('number'); + } finally { + server.close(); + } + }); +}); + +describe('GET /api/admin/health', () => { + it('returns 200 with ok, services array, and checkedAt', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { status, body } = await call('GET', '/api/admin/health'); + const b = body as { ok: boolean; services: { name: string; status: string }[]; checkedAt: string }; + expect(status).toBe(200); + expect(typeof b.ok).toBe('boolean'); + expect(Array.isArray(b.services)).toBe(true); + expect(typeof b.checkedAt).toBe('string'); + } finally { + server.close(); + } + }); +}); + +describe('GET /api/admin/users/:id — edge cases', () => { + it('user with no integrations and no tips has empty arrays and 0 count', async () => { + const { server, call } = await startServer(buildApp()); + try { + // user-2 has no integrations and no feedback seeded + const { status, body } = await call('GET', '/api/admin/users/user-2'); + const b = body as { + user: { id: string }; + integrations: unknown[]; + tipsServed: number; + recentFeedback: unknown[]; + }; + expect(status).toBe(200); + expect(b.integrations).toHaveLength(0); + expect(b.recentFeedback).toHaveLength(0); + } finally { + server.close(); + } + }); +}); + +describe('GET /api/admin/stats — field types', () => { + it('reactionsLast7d has correct action counts', async () => { + const { server, call } = await startServer(buildApp()); + try { + const { body } = await call('GET', '/api/admin/stats'); + const b = body as { reactionsLast7d: Record }; + // We seeded 'done' and 'snooze' feedback + expect(typeof b.reactionsLast7d['done']).toBe('number'); + expect(typeof b.reactionsLast7d['snooze']).toBe('number'); + } finally { + server.close(); + } + }); +}); diff --git a/services/api/src/routes/admin.ts b/services/api/src/routes/admin.ts index 9bea687..599da28 100644 --- a/services/api/src/routes/admin.ts +++ b/services/api/src/routes/admin.ts @@ -8,6 +8,8 @@ import { adminActions, tipScores, savedQueries, + simRuns, + simEvents, } from '../db/schema.js'; import { eq, desc, sql, gte, and, isNull, lt } from 'drizzle-orm'; import { requireAuth, AuthenticatedRequest } from '../middleware/session.js'; @@ -16,6 +18,16 @@ import { nanoid } from 'nanoid'; import { bus } from '../events/bus.js'; import { config } from '../config.js'; import { getShadowPolicies, setPolicyActive } from './recommender.js'; +import { spawn } from 'child_process'; +import { existsSync, readFileSync, unlinkSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// In-memory tracker for running sim processes +const _simProcesses = new Map(); const router: ExpressRouter = Router(); router.use(requireAuth, requireAdmin); @@ -606,4 +618,158 @@ router.delete('/saved-queries/:id', async (req: AuthenticatedRequest, res: Respo res.json({ ok: true }); }); +// --------------------------------------------------------------------------- +// POST /api/admin/simulate/start +// Spawn ml/experiments/sim/runner.py in the background; return run_id. +// --------------------------------------------------------------------------- +router.post('/simulate/start', async (req: AuthenticatedRequest, res: Response) => { + const { + nUsers = 5, + nRounds = 20, + tasksPerRound = 8, + useLlm = false, + judgeMode = 'rule', + policies = ['linucb-v1', 'egreedy-v1'], + } = req.body as { + nUsers?: number; + nRounds?: number; + tasksPerRound?: number; + useLlm?: boolean; + judgeMode?: 'rule' | 'llm' | 'claude-code'; + policies?: string[]; + }; + + if (policies.length < 2) { + res.status(400).json({ error: 'At least two policies required' }); + return; + } + + const id = nanoid(); + const now = new Date().toISOString(); + + await db.insert(simRuns).values({ + id, + policyA: policies[0], + policyB: policies[1], + nUsers, + nRounds, + tasksPerRound, + useLlm, + status: 'running', + createdAt: now, + }); + + const runnerPath = resolve(__dirname, '../../../../ml/experiments/sim/runner.py'); + const venvPython = resolve(__dirname, '../../../../ml/serving/.venv/bin/python'); + const pythonBin = existsSync(venvPython) ? venvPython : 'python3'; + const outPath = `/tmp/oo-sim-${id}.json`; + + const args = [ + runnerPath, + '--n-users', String(nUsers), + '--n-rounds', String(nRounds), + '--tasks-per-round', String(tasksPerRound), + '--ml-url', config.ML_SERVING_URL, + '--policies', ...policies, + '--out', outPath, + '--judge', judgeMode === 'llm' ? 'llm' : judgeMode === 'claude-code' ? 'rule' : 'rule', + // claude-code mode isn't auto-runnable from the API (requires human in the loop) + // it falls back to rule judge when triggered from the panel + ]; + + const child = spawn(pythonBin, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + + if (child.pid) { + _simProcesses.set(id, { pid: child.pid, startedAt: now }); + } + + // Capture stderr for debugging + const stderrLines: string[] = []; + child.stderr?.on('data', (d: Buffer) => stderrLines.push(d.toString())); + + child.on('exit', async (code) => { + _simProcesses.delete(id); + const finishedAt = new Date().toISOString(); + + if (code === 0 && existsSync(outPath)) { + try { + const raw = JSON.parse(readFileSync(outPath, 'utf-8')); + + // Bulk-insert sim events + const eventRows = (raw.events ?? []).map((ev: Record) => ({ + id: nanoid(), + runId: id, + round: Number(ev.round), + userId: String(ev.user_id), + persona: String(ev.persona), + policy: String(ev.policy), + tipContent: String(ev.tip_content), + priority: Number(ev.priority), + isOverdue: Boolean(ev.is_overdue), + action: String(ev.action), + dwellMs: ev.dwell_ms != null ? Number(ev.dwell_ms) : null, + rewardMilli: Math.round(Number(ev.reward) * 1000), + hour: Number(ev.hour), + dayOfWeek: Number(ev.day_of_week), + createdAt: now, + })); + + for (const row of eventRows) { + await db.insert(simEvents).values(row).catch(() => {}); + } + + await db.update(simRuns).set({ + status: 'done', + summaryJson: JSON.stringify(raw.summary), + winner: raw.winner, + personaBreakdownJson: JSON.stringify(raw.persona_breakdown), + finishedAt, + }).where(eq(simRuns.id, id)); + + try { unlinkSync(outPath); } catch { /* ignore */ } + } catch (e) { + await db.update(simRuns).set({ status: 'failed', finishedAt }).where(eq(simRuns.id, id)); + } + } else { + await db.update(simRuns).set({ status: 'failed', finishedAt }).where(eq(simRuns.id, id)); + } + }); + + res.json({ id, status: 'running' }); +}); + +// --------------------------------------------------------------------------- +// GET /api/admin/simulate/runs +// --------------------------------------------------------------------------- +router.get('/simulate/runs', async (_req: AuthenticatedRequest, res: Response) => { + const rows = await db + .select() + .from(simRuns) + .orderBy(desc(simRuns.createdAt)) + .limit(50); + res.json({ runs: rows }); +}); + +// --------------------------------------------------------------------------- +// GET /api/admin/simulate/:id +// --------------------------------------------------------------------------- +router.get('/simulate/:id', async (req: AuthenticatedRequest, res: Response) => { + const { id } = req.params as { id: string }; + const [run] = await db.select().from(simRuns).where(eq(simRuns.id, id)).limit(1); + if (!run) { + res.status(404).json({ error: 'Run not found' }); + return; + } + + const events = await db + .select() + .from(simEvents) + .where(eq(simEvents.runId, id)) + .orderBy(simEvents.round) + .limit(5000); + + const isRunning = _simProcesses.has(id); + res.json({ run: { ...run, isRunning }, events }); +}); + export { router as adminRouter }; diff --git a/services/api/src/routes/recommender.ts b/services/api/src/routes/recommender.ts index 4b9e982..38e5b4d 100644 --- a/services/api/src/routes/recommender.ts +++ b/services/api/src/routes/recommender.ts @@ -2,7 +2,7 @@ import { type Router as ExpressRouter, Router, Response } from 'express'; import { nanoid } from 'nanoid'; import { db } from '../db/index.js'; import { integrationTokens, tipFeedback, tipViews, tipScores } from '../db/schema.js'; -import { eq, and } from 'drizzle-orm'; +import { eq, and, desc } from 'drizzle-orm'; import { requireAuth, AuthenticatedRequest } from '../middleware/session.js'; import { config } from '../config.js'; import { bus } from '../events/bus.js'; @@ -105,7 +105,7 @@ async function fetchTodoistTasks(userId: string, accessToken: string): Promise { +): Promise<{ tipId: string; score: number; policy: string } | null> { const hour = new Date().getHours(); const dayOfWeek = new Date().getDay(); @@ -121,8 +121,9 @@ async function remotePolicy( context: { hour_of_day: hour, day_of_week: dayOfWeek }, }; + // Active policy: egreedy-v1 (selected over linucb-v1 after offline sim — ADR-0007) try { - const res = await fetch(`${config.ML_SERVING_URL}/score`, { + const res = await fetch(`${config.ML_SERVING_URL}/score/egreedy`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), @@ -130,7 +131,7 @@ async function remotePolicy( }); if (!res.ok) return null; const data = (await res.json()) as { tip_id: string; score: number }; - return { tipId: data.tip_id, score: data.score }; + return { tipId: data.tip_id, score: data.score, policy: 'egreedy-v1' }; } catch { return null; } @@ -178,7 +179,7 @@ router.post('/recommend', requireAuth, async (req: AuthenticatedRequest, res: Re return; } - const policy = scored ? 'linucb-v1' : 'random'; + const policy = scored ? scored.policy : 'random'; const servedAt = new Date().toISOString(); await db.insert(tipViews).values({ id: nanoid(), userId: req.userId!, tipId: tip.id, servedAt }); @@ -226,55 +227,85 @@ router.post('/recommend', requireAuth, async (req: AuthenticatedRequest, res: Re res.json({ tip }); }); +// --------------------------------------------------------------------------- +// Reward inference from action + dwell time +// +// Feedback is now 3 signals only: done / snooze / dismiss. +// "Helpfulness" is inferred from how long the user took to act on a tip: +// dismiss → -1.0 (clear rejection) +// snooze → +0.1 (tip noticed, timing off — mild positive) +// done < 15 s → -0.3 (almost certainly a stale task, not magic) +// done 15 s – 2 min → +1.0 (magic zone: user saw tip and acted) +// done 2 – 10 min → +0.6 (good: user engaged, acted in same session) +// done > 10 min → +0.3 (eventually done; tip may have helped, unclear) +// --------------------------------------------------------------------------- +function inferReward(action: string, dwellMs: number | null): number { + if (action === 'dismiss') return -1.0; + if (action === 'snooze') return 0.1; + // done — use dwell time + if (dwellMs === null || dwellMs < 0) return 0.5; // unknown dwell: neutral positive + if (dwellMs < 15_000) return -0.3; // stale / reflex + if (dwellMs < 120_000) return 1.0; // magic zone + if (dwellMs < 600_000) return 0.6; // good + return 0.3; // eventually +} + // --------------------------------------------------------------------------- // POST /api/tip/:id/feedback // --------------------------------------------------------------------------- router.post('/tip/:id/feedback', requireAuth, async (req: AuthenticatedRequest, res: Response) => { const { action } = req.body as { action: string }; const tipId = String(req.params.id); + const now = new Date(); - const validActions = ['done', 'dismiss', 'snooze', 'helpful', 'not_helpful']; + const validActions = ['done', 'dismiss', 'snooze']; if (!validActions.includes(action)) { res.status(400).json({ error: 'Invalid action' }); return; } + // Compute dwell time from the most recent tipViews record for this user+tip + let dwellMs: number | null = null; + const [lastView] = await db + .select({ servedAt: tipViews.servedAt }) + .from(tipViews) + .where(and(eq(tipViews.userId, req.userId!), eq(tipViews.tipId, tipId))) + .orderBy(desc(tipViews.servedAt)) + .limit(1); + + if (lastView?.servedAt) { + dwellMs = now.getTime() - new Date(lastView.servedAt).getTime(); + } + + const reward = inferReward(action, dwellMs); + await db.insert(tipFeedback).values({ id: nanoid(), userId: req.userId!, tipId, action, sourceId: tipId.startsWith('todoist:') ? tipId.slice(8) : null, - createdAt: new Date().toISOString(), + dwellMs: dwellMs !== null ? Math.round(dwellMs) : null, + rewardMilli: Math.round(reward * 1000), + createdAt: now.toISOString(), }); - // Map action to reward (helpful/not_helpful supplement behavioural signals) - const rewardMap: Record = { - done: 1.0, - helpful: 0.5, - snooze: 0.0, - not_helpful: -0.5, - dismiss: -1.0, - }; - const reward = rewardMap[action] ?? 0.0; - const task = taskCache.get(req.userId!)?.tasks.find((t) => t.id === tipId); - // Clear cache on behavioural actions (not on explicit helpful/not_helpful) - if (['done', 'dismiss', 'snooze'].includes(action)) { - taskCache.delete(req.userId!); - } + taskCache.delete(req.userId!); bus.publish('signals.tip.feedback', { userId: req.userId!, tipId, - action: action as 'done' | 'dismiss' | 'snooze' | 'helpful' | 'not_helpful', + action: action as 'done' | 'dismiss' | 'snooze', reward, - createdAt: new Date().toISOString(), + dwellMs, + createdAt: now.toISOString(), }); if (task) { - fetch(`${config.ML_SERVING_URL}/reward`, { + // Send reward to egreedy-v1 (active policy — ADR-0007) + fetch(`${config.ML_SERVING_URL}/reward/egreedy`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -282,6 +313,7 @@ router.post('/tip/:id/feedback', requireAuth, async (req: AuthenticatedRequest, tip_id: tipId, reward, features: task.features, + day_of_week: new Date().getDay(), }), }).catch(() => {}); } diff --git a/services/api/src/routes/user.ts b/services/api/src/routes/user.ts index 73f597c..cda9d7f 100644 --- a/services/api/src/routes/user.ts +++ b/services/api/src/routes/user.ts @@ -18,6 +18,7 @@ router.get('/me', requireAuth, async (req: AuthenticatedRequest, res: Response) email: user.email, name: user.name, image: user.image, + role: user.role, createdAt: user.createdAt, consentGiven: user.consentGiven, }); diff --git a/services/api/src/test/db.ts b/services/api/src/test/db.ts new file mode 100644 index 0000000..18cb3c6 --- /dev/null +++ b/services/api/src/test/db.ts @@ -0,0 +1,84 @@ +/** + * Creates an isolated in-memory SQLite DB with the full schema applied. + * Use this in tests instead of the shared `db` singleton. + */ +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import * as schema from '../db/schema.js'; + +export function makeTestDb() { + const sqlite = new Database(':memory:'); + sqlite.pragma('foreign_keys = ON'); + + sqlite.exec(` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + name TEXT, + image TEXT, + google_id TEXT UNIQUE, + role TEXT NOT NULL DEFAULT 'user', + consent_given INTEGER NOT NULL DEFAULT 0, + consent_at TEXT, + created_at TEXT NOT NULL, + deleted_at TEXT + ); + + CREATE TABLE IF NOT EXISTS integration_tokens ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + provider TEXT NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT, + expires_at TEXT, + connected_at TEXT NOT NULL, + UNIQUE(user_id, provider) + ); + + CREATE TABLE IF NOT EXISTS tip_feedback ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + tip_id TEXT NOT NULL, + action TEXT NOT NULL, + source_id TEXT, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS tip_views ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + tip_id TEXT NOT NULL, + served_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS push_subscriptions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + endpoint TEXT NOT NULL UNIQUE, + p256dh TEXT NOT NULL, + auth TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + expires_at TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS admin_actions ( + id TEXT PRIMARY KEY, + admin_id TEXT NOT NULL REFERENCES users(id), + action TEXT NOT NULL, + target_type TEXT, + target_id TEXT, + detail TEXT, + created_at TEXT NOT NULL + ); + `); + + return drizzle(sqlite, { schema }); +} + +export type TestDb = ReturnType; diff --git a/services/api/vitest.config.ts b/services/api/vitest.config.ts new file mode 100644 index 0000000..66349ac --- /dev/null +++ b/services/api/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['src/**'], + }, + }, +}); diff --git a/turbo.json b/turbo.json index 4143ddf..b52d4ce 100644 --- a/turbo.json +++ b/turbo.json @@ -18,6 +18,10 @@ "test": { "dependsOn": ["^build"] }, + "test:e2e": { + "dependsOn": ["build"], + "cache": false + }, "clean": { "cache": false }