diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b7c4e2..49531cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,3 +17,16 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm verify + + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v5 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm --filter @sentinel/dashboard exec playwright install --with-deps chromium + - run: pnpm test:e2e diff --git a/.gitignore b/.gitignore index fe1a6c9..735f595 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ node_modules/ dist/ coverage/ *.tsbuildinfo +playwright-report/ +test-results/ +.last-run.json # Local data *.sqlite diff --git a/README.md b/README.md index 40ff6fc..84a58a4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A self-hostable **verifying LLM gateway** β€” a drop-in, OpenAI-compatible proxy that **routes** (cheapest capable model + fallback), **semantically caches**, and **verifies** (deterministic guardrails inline + a local Ollama judge) every LLM call, with full OpenTelemetry tracing. Unlike after-the-fact observability tools, it can flag or block a bad response _before it returns_. -> 🚧 **Early development.** Product spec in [`PRP_SPEC.md`](./PRP_SPEC.md), phased build in [`ROADMAP.md`](./ROADMAP.md), contributor/agent guidance in [`CLAUDE.md`](./CLAUDE.md). Currently at **Phase 5 β€” verification: guardrails + judge** (a quality dashboard lands next). +> 🚧 **Early development.** Product spec in [`PRP_SPEC.md`](./PRP_SPEC.md), phased build in [`ROADMAP.md`](./ROADMAP.md), contributor/agent guidance in [`CLAUDE.md`](./CLAUDE.md). The gateway is feature-complete (proxy, tracing, cache, routing, verification) and now ships a **Phase 6** dashboard; hardening + launch (Phase 7) is next. ## What works today (Phase 1) @@ -125,6 +125,17 @@ Sentinel can check a response's quality **in the request path**, not just after Verdicts are queryable: `GET /traces?guardrailStatus=block`, `?judgeScoreMax=2`, `?promptFingerprint=…`. Inline guardrails apply to non-streaming responses; streamed responses are judged from their buffered output after they complete. +## Dashboard + +A read-only **React + Vite** dashboard (`packages/dashboard`) visualizes the trace API: request volume over time, error / cache-hit / fallback rates, latency p95, token usage, provider / model / status distributions, a judge-score histogram, guardrail breakdown, a recent-requests table, and quality regressions. It aggregates client-side from `GET /traces` + `GET /regression`, so it needs only your gateway's `SENTINEL_ADMIN_KEY`. + +```bash +pnpm dev # gateway on :8080 (set SENTINEL_ADMIN_KEY to enable the admin API) +pnpm dev:dashboard # dashboard on :5173 (dev-proxies the admin API to :8080) +``` + +Open , paste your admin key, and hit Refresh. The dashboard is end-to-end tested with Playwright (`pnpm test:e2e`). + ## Development ```bash diff --git a/eslint.config.js b/eslint.config.js index c1f6ff2..775dffa 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,6 +23,17 @@ export default tseslint.config( ], }, }, + { + // Dashboard React components (browser, JSX). Default exports allowed for entry/lazy chunks. + files: ['**/*.tsx'], + extends: [...tseslint.configs.strict, ...tseslint.configs.stylistic], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + }, + }, { // Tooling config files conventionally require a default export. files: ['**/*.config.ts'], diff --git a/package.json b/package.json index ded0887..c1f413c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "scripts": { "build": "tsc -p tsconfig.build.json", - "typecheck": "tsc -p tsconfig.json --noEmit", + "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p packages/dashboard/tsconfig.json --noEmit", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write .", @@ -18,8 +18,10 @@ "test": "vitest run", "test:watch": "vitest", "test:cov": "vitest run --coverage", - "test:e2e": "node -e \"console.log('e2e suite lands in ROADMAP Phase 6')\"", + "test:e2e": "pnpm --filter @sentinel/dashboard test:e2e", "dev": "tsx watch packages/gateway/src/main.ts", + "dev:dashboard": "pnpm --filter @sentinel/dashboard dev", + "build:dashboard": "pnpm --filter @sentinel/dashboard build", "start": "tsx packages/gateway/src/main.ts", "verify": "pnpm run typecheck && pnpm run lint && pnpm run test:cov", "ollama:pull": "ollama pull qwen2.5:7b && ollama pull nomic-embed-text" diff --git a/packages/dashboard/e2e/dashboard.spec.ts b/packages/dashboard/e2e/dashboard.spec.ts new file mode 100644 index 0000000..91fac41 --- /dev/null +++ b/packages/dashboard/e2e/dashboard.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; + +function sampleTrace(over: Record): Record { + return { + id: 'x', + traceId: 't', + timestamp: 1_700_000_000_000, + durationMs: 120, + model: 'gpt-4o-mini', + provider: 'openai', + stream: false, + status: 200, + promptTokens: 10, + completionTokens: 5, + totalTokens: 15, + errorType: null, + errorMessage: null, + apiKeyHash: null, + cacheHit: false, + routedProvider: null, + routedModel: null, + fallbackUsed: false, + retryCount: 0, + guardrailStatus: null, + guardrailViolations: null, + judgeScore: null, + judgeReason: null, + judgeError: null, + promptFingerprint: null, + ...over, + }; +} + +const traces = [ + sampleTrace({ id: 'a', status: 200, cacheHit: true, judgeScore: 5 }), + sampleTrace({ id: 'b', status: 500, errorType: 'upstream_error' }), + sampleTrace({ id: 'c', status: 200, provider: 'groq', fallbackUsed: true, judgeScore: 3 }), +]; + +test('renders aggregated stats from the trace API', async ({ page }) => { + await page.route('**/traces*', (route) => route.fulfill({ json: traces })); + await page.route('**/regression*', (route) => route.fulfill({ json: [] })); + await page.addInitScript(() => { + window.localStorage.setItem('sentinel.adminKey', 'test-admin'); + window.localStorage.setItem('sentinel.baseUrl', ''); + }); + + await page.goto('/'); + + await expect(page.getByRole('heading', { name: 'Sentinel' })).toBeVisible(); + await expect(page.getByTestId('stat-total')).toContainText('3'); + await expect(page.getByTestId('stat-cache')).toContainText('33'); + // 'groq' appears in both the provider bar (with a title) and the table; assert the bar. + await expect(page.getByTitle('groq')).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Recent requests' })).toBeVisible(); + await expect(page.getByTestId('stat-judge')).toContainText('4'); // mean of [5,3] +}); diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html new file mode 100644 index 0000000..3c2ef8b --- /dev/null +++ b/packages/dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Sentinel Dashboard + + +
+ + + diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json new file mode 100644 index 0000000..e9330c2 --- /dev/null +++ b/packages/dashboard/package.json @@ -0,0 +1,24 @@ +{ + "name": "@sentinel/dashboard", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Sentinel dashboard β€” a read-only React UI over the gateway's admin trace API.", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --port 4173", + "test:e2e": "playwright test" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.49.1", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "vite": "^6.0.7" + } +} diff --git a/packages/dashboard/playwright.config.ts b/packages/dashboard/playwright.config.ts new file mode 100644 index 0000000..cb72c55 --- /dev/null +++ b/packages/dashboard/playwright.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from '@playwright/test'; + +// Hermetic E2E: builds the dashboard, serves the static preview, and the spec +// stubs the gateway's admin API with seeded traces (no real gateway needed). +export default defineConfig({ + testDir: './e2e', + timeout: 30_000, + use: { baseURL: 'http://localhost:4173' }, + webServer: { + command: 'pnpm build && pnpm preview', + url: 'http://localhost:4173', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/packages/dashboard/src/App.tsx b/packages/dashboard/src/App.tsx new file mode 100644 index 0000000..bbb39cc --- /dev/null +++ b/packages/dashboard/src/App.tsx @@ -0,0 +1,148 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { fetchRegression, fetchTraces } from './api'; +import type { ApiConfig } from './api'; +import { computeStats } from './aggregate'; +import type { RegressionGroup, TraceRecord } from './types'; +import { + BarChart, + Histogram, + RegressionPanel, + Sparkline, + StatCard, + TracesTable, +} from './components'; + +const KEY_ADMIN = 'sentinel.adminKey'; +const KEY_BASE = 'sentinel.baseUrl'; + +function pct(value: number): string { + return `${String(Math.round(value * 1000) / 10)}%`; +} + +export function App() { + const [baseUrl, setBaseUrl] = useState(() => localStorage.getItem(KEY_BASE) ?? ''); + const [adminKey, setAdminKey] = useState(() => localStorage.getItem(KEY_ADMIN) ?? ''); + const [traces, setTraces] = useState([]); + const [regression, setRegression] = useState([]); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const load = useCallback(async () => { + if (adminKey === '') { + setError('Enter your gateway admin key to load traces.'); + return; + } + const config: ApiConfig = { baseUrl, adminKey }; + setLoading(true); + setError(null); + try { + const [t, r] = await Promise.all([fetchTraces(config), fetchRegression(config)]); + setTraces(t); + setRegression(r); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }, [baseUrl, adminKey]); + + useEffect(() => { + localStorage.setItem(KEY_BASE, baseUrl); + localStorage.setItem(KEY_ADMIN, adminKey); + }, [baseUrl, adminKey]); + + // Auto-load once on mount if an admin key is already stored; later loads are via Refresh. + useEffect(() => { + if (adminKey !== '') void load(); + }, []); + + const stats = useMemo(() => computeStats(traces), [traces]); + + return ( +
+
+

Sentinel

+
+ { + setBaseUrl(e.target.value); + }} + /> + { + setAdminKey(e.target.value); + }} + /> + +
+
+ + {error !== null ? ( +
+ {error} +
+ ) : null} + +
+ + + + + + + +
+ +
+ + + + + + +
+ + + +
+ ); +} diff --git a/packages/dashboard/src/aggregate.test.ts b/packages/dashboard/src/aggregate.test.ts new file mode 100644 index 0000000..1e5d596 --- /dev/null +++ b/packages/dashboard/src/aggregate.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from 'vitest'; +import { computeStats, percentile, EMPTY_STATS } from './aggregate'; +import type { TraceRecord } from './types'; + +function trace(over: Partial): TraceRecord { + return { + id: 'id', + traceId: 'tid', + timestamp: 1_000_000, + durationMs: 100, + model: 'gpt-4o-mini', + provider: 'openai', + stream: false, + status: 200, + promptTokens: 10, + completionTokens: 5, + totalTokens: 15, + errorType: null, + errorMessage: null, + apiKeyHash: null, + cacheHit: false, + routedProvider: null, + routedModel: null, + fallbackUsed: false, + retryCount: 0, + guardrailStatus: null, + guardrailViolations: null, + judgeScore: null, + judgeReason: null, + judgeError: null, + promptFingerprint: null, + ...over, + }; +} + +describe('percentile', () => { + it('returns 0 for an empty array', () => { + expect(percentile([], 95)).toBe(0); + }); + it('computes nearest-rank percentiles', () => { + expect(percentile([10, 20, 30, 40, 50], 95)).toBe(50); + expect(percentile([10, 20, 30, 40, 50], 50)).toBe(30); + }); +}); + +describe('computeStats', () => { + it('returns the empty stats for no traces', () => { + expect(computeStats([])).toEqual(EMPTY_STATS); + }); + + it('aggregates rates, tokens, latency and distributions', () => { + const stats = computeStats([ + trace({ status: 200, cacheHit: true, totalTokens: 100, durationMs: 50 }), + trace({ status: 500, errorType: 'upstream_error', totalTokens: 0, durationMs: 200 }), + trace({ + status: 200, + fallbackUsed: true, + provider: 'groq', + totalTokens: 50, + durationMs: 100, + }), + ]); + expect(stats.total).toBe(3); + expect(stats.errorCount).toBe(1); + expect(stats.cacheHits).toBe(1); + expect(stats.fallbacks).toBe(1); + expect(stats.totalTokens).toBe(150); + expect(stats.byProvider.find((c) => c.label === 'openai')?.count).toBe(2); + expect(stats.byProvider.find((c) => c.label === 'groq')?.count).toBe(1); + expect(stats.byStatusClass.find((c) => c.label === '5xx')?.count).toBe(1); + }); + + it('builds the judge histogram and average', () => { + const stats = computeStats([ + trace({ judgeScore: 5 }), + trace({ judgeScore: 5 }), + trace({ judgeScore: 3 }), + ]); + expect(stats.avgJudgeScore).toBeCloseTo(4.33, 2); + expect(stats.judgeHistogram.find((b) => b.score === 5)?.count).toBe(2); + expect(stats.judgeHistogram.find((b) => b.score === 3)?.count).toBe(1); + expect(stats.judgeScoredCount).toBe(3); + }); + + it('buckets requests over time', () => { + const stats = computeStats( + [trace({ timestamp: 0 }), trace({ timestamp: 30_000 }), trace({ timestamp: 90_000 })], + 60_000, + ); + expect(stats.overTime).toHaveLength(2); + expect(stats.overTime[0]?.count).toBe(2); + expect(stats.overTime[1]?.count).toBe(1); + }); +}); diff --git a/packages/dashboard/src/aggregate.ts b/packages/dashboard/src/aggregate.ts new file mode 100644 index 0000000..4501381 --- /dev/null +++ b/packages/dashboard/src/aggregate.ts @@ -0,0 +1,159 @@ +import type { TraceRecord } from './types'; + +export interface Count { + label: string; + count: number; +} + +export interface TimeBucket { + bucket: number; + count: number; + tokens: number; + errors: number; +} + +export interface ScoreBin { + score: number; + count: number; +} + +export interface Stats { + total: number; + errorCount: number; + errorRate: number; + cacheHits: number; + cacheHitRate: number; + fallbacks: number; + fallbackRate: number; + totalTokens: number; + avgLatencyMs: number; + p95LatencyMs: number; + judgeScoredCount: number; + avgJudgeScore: number | null; + byProvider: Count[]; + byModel: Count[]; + byStatusClass: Count[]; + byGuardrail: Count[]; + judgeHistogram: ScoreBin[]; + overTime: TimeBucket[]; +} + +const emptyHistogram = (): ScoreBin[] => [1, 2, 3, 4, 5].map((score) => ({ score, count: 0 })); + +export const EMPTY_STATS: Stats = { + total: 0, + errorCount: 0, + errorRate: 0, + cacheHits: 0, + cacheHitRate: 0, + fallbacks: 0, + fallbackRate: 0, + totalTokens: 0, + avgLatencyMs: 0, + p95LatencyMs: 0, + judgeScoredCount: 0, + avgJudgeScore: null, + byProvider: [], + byModel: [], + byStatusClass: [], + byGuardrail: [], + judgeHistogram: emptyHistogram(), + overTime: [], +}; + +/** Nearest-rank percentile of an unsorted array. Returns 0 for an empty input. */ +export function percentile(values: number[], p: number): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const rank = Math.ceil((p / 100) * sorted.length) - 1; + const index = Math.min(sorted.length - 1, Math.max(0, rank)); + return sorted[index] ?? 0; +} + +function tally(items: (string | null)[]): Count[] { + const counts = new Map(); + for (const item of items) { + const label = item ?? 'unknown'; + counts.set(label, (counts.get(label) ?? 0) + 1); + } + return [...counts.entries()] + .map(([label, count]) => ({ label, count })) + .sort((a, b) => b.count - a.count); +} + +function statusClass(status: number): string { + if (status >= 500) return '5xx'; + if (status >= 400) return '4xx'; + if (status >= 200) return '2xx'; + return 'other'; +} + +/** Reduces a window of traces into the dashboard's summary statistics. Pure. */ +export function computeStats(traces: TraceRecord[], bucketMs = 60_000): Stats { + if (traces.length === 0) return { ...EMPTY_STATS, judgeHistogram: emptyHistogram() }; + + const total = traces.length; + let errorCount = 0; + let cacheHits = 0; + let fallbacks = 0; + let totalTokens = 0; + const latencies: number[] = []; + const judgeScores: number[] = []; + const histogram = new Map([ + [1, 0], + [2, 0], + [3, 0], + [4, 0], + [5, 0], + ]); + const buckets = new Map(); + + for (const t of traces) { + const isError = t.status >= 400; + if (isError) errorCount++; + if (t.cacheHit) cacheHits++; + if (t.fallbackUsed) fallbacks++; + totalTokens += t.totalTokens ?? 0; + latencies.push(t.durationMs); + if (t.judgeScore !== null) { + judgeScores.push(t.judgeScore); + const rounded = Math.round(t.judgeScore); + if (histogram.has(rounded)) histogram.set(rounded, (histogram.get(rounded) ?? 0) + 1); + } + const key = Math.floor(t.timestamp / bucketMs) * bucketMs; + const bucket = buckets.get(key) ?? { bucket: key, count: 0, tokens: 0, errors: 0 }; + bucket.count++; + bucket.tokens += t.totalTokens ?? 0; + if (isError) bucket.errors++; + buckets.set(key, bucket); + } + + const avgLatency = latencies.reduce((sum, v) => sum + v, 0) / total; + const avgJudge = + judgeScores.length > 0 + ? Math.round((judgeScores.reduce((sum, v) => sum + v, 0) / judgeScores.length) * 100) / 100 + : null; + + return { + total, + errorCount, + errorRate: errorCount / total, + cacheHits, + cacheHitRate: cacheHits / total, + fallbacks, + fallbackRate: fallbacks / total, + totalTokens, + avgLatencyMs: Math.round(avgLatency * 100) / 100, + p95LatencyMs: Math.round(percentile(latencies, 95) * 100) / 100, + judgeScoredCount: judgeScores.length, + avgJudgeScore: avgJudge, + byProvider: tally(traces.map((t) => t.routedProvider ?? t.provider)), + byModel: tally(traces.map((t) => t.routedModel ?? t.model)), + byStatusClass: tally(traces.map((t) => statusClass(t.status))), + byGuardrail: tally( + traces.filter((t) => t.guardrailStatus !== null).map((t) => t.guardrailStatus), + ), + judgeHistogram: [1, 2, 3, 4, 5].map((score) => ({ score, count: histogram.get(score) ?? 0 })), + overTime: [...buckets.values()].sort((a, b) => a.bucket - b.bucket), + }; +} diff --git a/packages/dashboard/src/api.test.ts b/packages/dashboard/src/api.test.ts new file mode 100644 index 0000000..b610c12 --- /dev/null +++ b/packages/dashboard/src/api.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { fetchTraces, fetchRegression } from './api'; +import type { ApiConfig } from './api'; + +const config: ApiConfig = { baseUrl: 'http://gw', adminKey: 'admin' }; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('api client', () => { + it('fetchTraces sends the admin bearer and parses JSON', async () => { + const fetchMock = vi.fn((_url: string, _init: RequestInit) => + Promise.resolve(new Response(JSON.stringify([{ id: 'a' }]), { status: 200 })), + ); + vi.stubGlobal('fetch', fetchMock); + + const traces = await fetchTraces(config, 10); + + expect(traces).toEqual([{ id: 'a' }]); + const call = fetchMock.mock.calls[0]!; + expect(call[0]).toBe('http://gw/traces?limit=10'); + expect(call[1].headers).toEqual({ authorization: 'Bearer admin' }); + }); + + it('fetchRegression hits /regression', async () => { + const fetchMock = vi.fn((_url: string, _init: RequestInit) => + Promise.resolve(new Response('[]', { status: 200 })), + ); + vi.stubGlobal('fetch', fetchMock); + + expect(await fetchRegression(config)).toEqual([]); + expect(fetchMock.mock.calls[0]![0]).toBe('http://gw/regression'); + }); + + it('throws on a non-OK response', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(() => + Promise.resolve(new Response('nope', { status: 401, statusText: 'Unauthorized' })), + ), + ); + await expect(fetchTraces(config)).rejects.toThrow(/401/); + }); +}); diff --git a/packages/dashboard/src/api.ts b/packages/dashboard/src/api.ts new file mode 100644 index 0000000..e891e01 --- /dev/null +++ b/packages/dashboard/src/api.ts @@ -0,0 +1,34 @@ +import type { RegressionGroup, TraceRecord } from './types'; + +export interface ApiConfig { + /** Gateway origin, e.g. `http://localhost:8080`, or `''` for same-origin (dev proxy). */ + baseUrl: string; + /** The gateway's `SENTINEL_ADMIN_KEY`. */ + adminKey: string; +} + +async function getJson(path: string, config: ApiConfig, signal?: AbortSignal): Promise { + const res = await fetch(`${config.baseUrl}${path}`, { + headers: { authorization: `Bearer ${config.adminKey}` }, + ...(signal ? { signal } : {}), + }); + if (!res.ok) { + throw new Error(`Request to ${path} failed: ${res.status} ${res.statusText}`); + } + return (await res.json()) as T; +} + +export function fetchTraces( + config: ApiConfig, + limit = 500, + signal?: AbortSignal, +): Promise { + return getJson(`/traces?limit=${String(limit)}`, config, signal); +} + +export function fetchRegression( + config: ApiConfig, + signal?: AbortSignal, +): Promise { + return getJson('/regression', config, signal); +} diff --git a/packages/dashboard/src/components.tsx b/packages/dashboard/src/components.tsx new file mode 100644 index 0000000..81456da --- /dev/null +++ b/packages/dashboard/src/components.tsx @@ -0,0 +1,167 @@ +import type { Count, ScoreBin, TimeBucket } from './aggregate'; +import type { RegressionGroup, TraceRecord } from './types'; + +export function StatCard(props: { label: string; value: string; sub?: string; testId?: string }) { + return ( +
+
{props.label}
+
{props.value}
+ {props.sub !== undefined ?
{props.sub}
: null} +
+ ); +} + +export function BarChart(props: { title: string; data: Count[] }) { + const max = Math.max(1, ...props.data.map((d) => d.count)); + return ( +
+

{props.title}

+ {props.data.length === 0 ? ( +
no data
+ ) : ( +
    + {props.data.map((d) => ( +
  • + + {d.label} + + + + + {d.count} +
  • + ))} +
+ )} +
+ ); +} + +export function Histogram(props: { title: string; bins: ScoreBin[] }) { + const max = Math.max(1, ...props.bins.map((b) => b.count)); + return ( +
+

{props.title}

+
+ {props.bins.map((b) => ( +
+
+
{b.score}
+
+ ))} +
+
+ ); +} + +export function Sparkline(props: { title: string; buckets: TimeBucket[] }) { + const pts = props.buckets; + const width = 280; + const height = 60; + const max = Math.max(1, ...pts.map((p) => p.count)); + const path = pts + .map((p, i) => { + const x = pts.length > 1 ? (i / (pts.length - 1)) * width : 0; + const y = height - (p.count / max) * height; + return `${i === 0 ? 'M' : 'L'}${String(Math.round(x))},${String(Math.round(y))}`; + }) + .join(' '); + return ( +
+

{props.title}

+ {pts.length > 1 ? ( + + + + ) : ( +
not enough data yet
+ )} +
+ ); +} + +export function TracesTable(props: { traces: TraceRecord[] }) { + return ( +
+

Recent requests

+
+ + + + + + + + + + + + + + + + {props.traces.slice(0, 50).map((t) => ( + = 400 ? 'err' : ''}> + + + + + + + + + + + ))} + +
timemodelproviderstatustokensmscacheguardrailjudge
{new Date(t.timestamp).toLocaleTimeString()}{t.routedModel ?? t.model ?? 'β€”'}{t.routedProvider ?? t.provider ?? 'β€”'}{t.status}{t.totalTokens ?? 'β€”'}{Math.round(t.durationMs)}{t.cacheHit ? 'βœ“' : ''}{t.guardrailStatus ?? 'β€”'}{t.judgeScore ?? 'β€”'}
+
+
+ ); +} + +export function RegressionPanel(props: { groups: RegressionGroup[] }) { + return ( +
+

Quality regressions (judge score by prompt Γ— model)

+ {props.groups.length === 0 ? ( +
no judge-scored traces yet
+ ) : ( +
+ + + + + + + + + + + + + {props.groups.map((g) => ( + + + + + + + + + ))} + +
fingerprintmodelnmeanminmax
{g.promptFingerprint.slice(0, 12)}{g.model ?? 'β€”'}{g.count}{g.meanScore}{g.minScore}{g.maxScore}
+
+ )} +
+ ); +} diff --git a/packages/dashboard/src/main.tsx b/packages/dashboard/src/main.tsx new file mode 100644 index 0000000..eca62cc --- /dev/null +++ b/packages/dashboard/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import './styles.css'; + +const container = document.getElementById('root'); +if (container === null) throw new Error('#root element not found'); + +createRoot(container).render( + + + , +); diff --git a/packages/dashboard/src/styles.css b/packages/dashboard/src/styles.css new file mode 100644 index 0000000..24683f8 --- /dev/null +++ b/packages/dashboard/src/styles.css @@ -0,0 +1,205 @@ +:root { + --bg: #0d1117; + --panel: #161b22; + --border: #30363d; + --text: #e6edf3; + --muted: #8b949e; + --accent: #2f81f7; + --err: #f85149; +} +* { + box-sizing: border-box; +} +body { + margin: 0; + background: var(--bg); + color: var(--text); + font: + 14px/1.5 system-ui, + -apple-system, + 'Segoe UI', + Roboto, + sans-serif; +} +.app { + max-width: 1200px; + margin: 0 auto; + padding: 16px; +} +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} +.topbar h1 { + margin: 0; + font-size: 20px; + letter-spacing: 0.5px; +} +.controls { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.controls input { + background: var(--panel); + border: 1px solid var(--border); + color: var(--text); + padding: 6px 10px; + border-radius: 6px; +} +.controls button { + background: var(--accent); + color: #fff; + border: 0; + padding: 6px 14px; + border-radius: 6px; + cursor: pointer; +} +.controls button:disabled { + opacity: 0.6; + cursor: default; +} +.banner.err { + background: rgba(248, 81, 73, 0.15); + border: 1px solid var(--err); + color: #ffb3ae; + padding: 10px 12px; + border-radius: 6px; + margin: 12px 0; +} +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + padding: 14px; +} +.card h3 { + margin: 0 0 10px; + font-size: 12px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} +.stats-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 12px; + margin: 16px 0; +} +.stat-label { + color: var(--muted); + font-size: 12px; +} +.stat-value { + font-size: 26px; + font-weight: 600; + margin-top: 4px; +} +.stat-sub { + color: var(--muted); + font-size: 12px; + margin-top: 2px; +} +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 12px; +} +.bars { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 6px; +} +.bars li { + display: grid; + grid-template-columns: 90px 1fr 40px; + align-items: center; + gap: 8px; +} +.bar-label { + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.bar-track { + background: #21262d; + border-radius: 4px; + height: 14px; + overflow: hidden; +} +.bar-fill { + display: block; + height: 100%; + background: var(--accent); +} +.bar-count { + text-align: right; + color: var(--muted); +} +.histogram { + display: flex; + align-items: flex-end; + gap: 8px; + height: 90px; +} +.hist-col { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + justify-content: flex-end; +} +.hist-bar { + width: 70%; + background: var(--accent); + border-radius: 3px 3px 0 0; + min-height: 2px; +} +.hist-axis { + color: var(--muted); + margin-top: 4px; +} +.spark { + width: 100%; + height: 60px; + color: var(--accent); +} +.card.wide { + margin-top: 12px; +} +.empty { + color: var(--muted); + font-style: italic; + padding: 8px 0; +} +table { + width: 100%; + border-collapse: collapse; +} +th, +td { + text-align: left; + padding: 6px 8px; + border-bottom: 1px solid var(--border); +} +th { + color: var(--muted); + font-weight: 500; + font-size: 12px; +} +tr.err td { + color: #ffb3ae; +} +.mono { + font-family: ui-monospace, monospace; +} +.table-wrap { + overflow-x: auto; +} diff --git a/packages/dashboard/src/types.ts b/packages/dashboard/src/types.ts new file mode 100644 index 0000000..f38da36 --- /dev/null +++ b/packages/dashboard/src/types.ts @@ -0,0 +1,41 @@ +/** + * Local mirror of the gateway's trace contract + * (`packages/gateway/src/telemetry/trace.ts` and `verify/regression.ts`). + * Kept here so the UI never depends on the gateway's build. + */ +export interface TraceRecord { + id: string; + traceId: string; + timestamp: number; + durationMs: number; + model: string | null; + provider: string | null; + stream: boolean; + status: number; + promptTokens: number | null; + completionTokens: number | null; + totalTokens: number | null; + errorType: string | null; + errorMessage: string | null; + apiKeyHash: string | null; + cacheHit: boolean; + routedProvider: string | null; + routedModel: string | null; + fallbackUsed: boolean; + retryCount: number; + guardrailStatus: string | null; + guardrailViolations: string | null; + judgeScore: number | null; + judgeReason: string | null; + judgeError: string | null; + promptFingerprint: string | null; +} + +export interface RegressionGroup { + promptFingerprint: string; + model: string | null; + count: number; + meanScore: number; + minScore: number; + maxScore: number; +} diff --git a/packages/dashboard/src/vite-env.d.ts b/packages/dashboard/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/dashboard/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/dashboard/tsconfig.json b/packages/dashboard/tsconfig.json new file mode 100644 index 0000000..6b24087 --- /dev/null +++ b/packages/dashboard/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "jsx": "react-jsx", + "noEmit": true + }, + "include": ["src"] +} diff --git a/packages/dashboard/vite.config.ts b/packages/dashboard/vite.config.ts new file mode 100644 index 0000000..7a9f8c9 --- /dev/null +++ b/packages/dashboard/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// In dev, proxy the admin read APIs to a locally-running gateway so the app can +// use a same-origin base URL (no CORS). In prod, point the app at your gateway. +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/traces': 'http://localhost:8080', + '/regression': 'http://localhost:8080', + }, + }, + preview: { port: 4173 }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35cb142..040eb85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,31 @@ importers: specifier: ^2.1.8 version: 2.1.9(@types/node@22.20.0) + packages/dashboard: + dependencies: + react: + specifier: ^19.0.0 + version: 19.2.7 + react-dom: + specifier: ^19.0.0 + version: 19.2.7(react@19.2.7) + devDependencies: + '@playwright/test': + specifier: ^1.49.1 + version: 1.61.1 + '@types/react': + specifier: ^19.0.2 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.0.2 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.3(@types/node@22.20.0)(tsx@4.22.4)) + vite: + specifier: ^6.0.7 + version: 6.4.3(@types/node@22.20.0)(tsx@4.22.4) + packages/gateway: dependencies: '@opentelemetry/api': @@ -85,6 +110,44 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -93,11 +156,39 @@ packages: resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -111,6 +202,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -123,6 +220,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.28.1': resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} @@ -135,6 +238,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.28.1': resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} @@ -147,6 +256,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.28.1': resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} @@ -159,6 +274,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.28.1': resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} @@ -171,6 +292,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.28.1': resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} @@ -183,6 +310,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} @@ -195,6 +328,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} @@ -207,6 +346,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.28.1': resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} @@ -219,6 +364,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.28.1': resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} @@ -231,6 +382,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.28.1': resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} @@ -243,6 +400,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.28.1': resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} @@ -255,6 +418,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.28.1': resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} @@ -267,6 +436,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.28.1': resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} @@ -279,6 +454,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.28.1': resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} @@ -291,6 +472,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.28.1': resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} @@ -303,12 +490,24 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.28.1': resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.28.1': resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} @@ -321,12 +520,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} @@ -339,12 +550,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/openharmony-arm64@0.28.1': resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} @@ -357,6 +580,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.28.1': resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} @@ -369,6 +598,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.28.1': resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} @@ -381,6 +616,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.28.1': resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} @@ -393,6 +634,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.28.1': resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} @@ -486,6 +733,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -575,6 +825,14 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/rollup-android-arm-eabi@4.62.2': resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} cpu: [arm] @@ -700,6 +958,18 @@ packages: cpu: [x64] os: [win32] + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} @@ -712,6 +982,14 @@ packages: '@types/node@22.20.0': resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@typescript-eslint/eslint-plugin@8.62.0': resolution: {integrity: sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -771,6 +1049,12 @@ packages: resolution: {integrity: sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/coverage-v8@2.1.9': resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} peerDependencies: @@ -876,6 +1160,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.40: + resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} + engines: {node: '>=6.0.0'} + hasBin: true + better-sqlite3@11.10.0: resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} @@ -895,6 +1184,11 @@ packages: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} + browserslist@4.28.4: + resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -906,6 +1200,9 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -931,6 +1228,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -939,6 +1239,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -978,6 +1281,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.380: + resolution: {integrity: sha512-W6d5AbuEoRayO447cqrg6lKJIlscgRnnxOZl/08kfV71BQDoEBC7Wwis68z87LjyK6f4kWyTaubuDbhHKrZkbA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -995,11 +1301,20 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1132,11 +1447,20 @@ 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} os: [darwin] + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -1223,10 +1547,18 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.3.0: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1242,6 +1574,11 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1265,6 +1602,9 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1318,6 +1658,10 @@ packages: resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} engines: {node: '>=10'} + node-releases@2.0.50: + resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + engines: {node: '>=18'} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -1380,6 +1724,16 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + postcss@8.5.15: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} @@ -1419,6 +1773,19 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -1465,9 +1832,16 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -1619,6 +1993,12 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1661,6 +2041,46 @@ packages: terser: optional: true + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + 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@2.1.9: resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1711,6 +2131,9 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -1725,14 +2148,113 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -1743,147 +2265,225 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/aix-ppc64@0.28.1': optional: true '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm64@0.28.1': optional: true '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-arm@0.28.1': optional: true '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/android-x64@0.28.1': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.28.1': optional: true '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.28.1': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.28.1': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.28.1': optional: true '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.28.1': optional: true '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-arm@0.28.1': optional: true '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-ia32@0.28.1': optional: true '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-loong64@0.28.1': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.28.1': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.28.1': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.28.1': optional: true '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-s390x@0.28.1': optional: true '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.28.1': optional: true + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-arm64@0.28.1': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.28.1': optional: true + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-arm64@0.28.1': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.28.1': optional: true + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/openharmony-arm64@0.28.1': optional: true '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.28.1': optional: true '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.28.1': optional: true '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-ia32@0.28.1': optional: true '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.25.12': + optional: true + '@esbuild/win32-x64@0.28.1': optional: true @@ -1988,6 +2588,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2078,6 +2683,12 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.62.2': optional: true @@ -2153,6 +2764,27 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + '@types/better-sqlite3@7.6.13': dependencies: '@types/node': 22.20.0 @@ -2165,6 +2797,14 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + '@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2256,6 +2896,18 @@ snapshots: '@typescript-eslint/types': 8.62.0 eslint-visitor-keys: 5.0.1 + '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@22.20.0)(tsx@4.22.4))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.3(@types/node@22.20.0)(tsx@4.22.4) + transitivePeerDependencies: + - supports-color + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.20.0))': dependencies: '@ampproject/remapping': 2.3.0 @@ -2367,6 +3019,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.40: {} + better-sqlite3@11.10.0: dependencies: bindings: 1.5.0 @@ -2395,6 +3049,14 @@ snapshots: dependencies: balanced-match: 4.0.4 + browserslist@4.28.4: + dependencies: + baseline-browser-mapping: 2.10.40 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.380 + node-releases: 2.0.50 + update-browserslist-db: 1.2.3(browserslist@4.28.4) + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -2404,6 +3066,8 @@ snapshots: callsites@3.1.0: {} + caniuse-lite@1.0.30001799: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -2429,6 +3093,8 @@ snapshots: concat-map@0.0.1: {} + convert-source-map@2.0.0: {} + cookie@1.1.1: {} cross-spawn@7.0.6: @@ -2437,6 +3103,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.2.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2459,6 +3127,8 @@ snapshots: eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.380: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -2495,6 +3165,35 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -2524,6 +3223,8 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} eslint-config-prettier@9.1.2(eslint@9.39.4): @@ -2686,9 +3387,14 @@ snapshots: fs-constants@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true + gensync@1.0.0-beta.2: {} + github-from-package@0.0.0: {} glob-parent@6.0.2: @@ -2766,10 +3472,14 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + js-tokens@4.0.0: {} + js-yaml@4.3.0: dependencies: argparse: 2.0.1 + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-schema-ref-resolver@3.0.0: @@ -2782,6 +3492,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -2807,6 +3519,10 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2853,6 +3569,8 @@ snapshots: dependencies: semver: 7.8.5 + node-releases@2.0.50: {} + on-exit-leak-free@2.1.2: {} once@1.4.0: @@ -2919,6 +3637,14 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.2.0 + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + postcss@8.5.15: dependencies: nanoid: 3.3.15 @@ -2964,6 +3690,15 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-refresh@0.17.0: {} + + react@19.2.7: {} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -3023,8 +3758,12 @@ snapshots: safe-stable-stringify@2.5.0: {} + scheduler@0.27.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} + semver@7.8.5: {} set-cookie-parser@2.7.2: {} @@ -3166,6 +3905,12 @@ snapshots: undici-types@6.21.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.4): + dependencies: + browserslist: 4.28.4 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -3199,6 +3944,19 @@ snapshots: '@types/node': 22.20.0 fsevents: 2.3.3 + vite@6.4.3(@types/node@22.20.0)(tsx@4.22.4): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.0 + fsevents: 2.3.3 + tsx: 4.22.4 + vitest@2.1.9(@types/node@22.20.0): dependencies: '@vitest/expect': 2.1.9 @@ -3259,6 +4017,8 @@ snapshots: wrappy@1.0.2: {} + yallist@3.1.1: {} + yocto-queue@0.1.0: {} zod@3.25.76: {} diff --git a/tsconfig.json b/tsconfig.json index f093c72..4b4126c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "noEmit": true }, - "include": ["packages/*/src/**/*.ts", "packages/*/test/**/*.ts", "vitest.config.ts"] + "include": ["packages/gateway/src/**/*.ts", "vitest.config.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts index 14bf4a4..7d12fa6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ include: ['packages/**/*.test.ts'], coverage: { provider: 'v8', - include: ['packages/*/src/**/*.ts'], + include: ['packages/gateway/src/**/*.ts'], exclude: [ '**/*.test.ts', '**/index.ts',