diff --git a/CHANGELOG.md b/CHANGELOG.md index bf347159..3625bd5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Write each change in both `### English` and `### 中文` under `## Unreleased`. ### English +- Show request volume, success rate, latency, tokens, and hourly traffic on Overview, with 1h / 24h / 7d windows from SQLite request history - Shrink Accounts cards: denser identity row, a compact runtime meter, and a quota fill that animates remaining credits - Show page skeletons again when the console refresh button is used, instead of leaving stale cards on screen - Show a list skeleton on Logs while filters, pagination, or refresh are loading, without replacing the filter bar @@ -17,6 +18,7 @@ Write each change in both `### English` and `### 中文` under `## Unreleased`. ### 中文 +- 概览页展示请求量、成功率、延迟、token 与按小时流量,时间窗口为 1 小时 / 24 小时 / 7 天,数据来自 SQLite 请求历史 - 账号卡片改为更紧凑的身份行:运行状态用短柱状指示,额度用填充条显示剩余量 - 控制台点刷新时重新显示骨架屏,不再把旧卡片留在页面上 - 日志页筛选、翻页或刷新时,下方列表显示骨架屏,筛选栏保持不动 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 93397bf3..f4d43fc6 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -417,7 +417,7 @@ Keep the menu short. Login is a gate, not a nav item. | Route | Nav | Job | |-------|-----|-----| | `/login` | no | Console password | -| `/` | Overview | Runtime pulse | +| `/` | Overview | Runtime pulse + request stats | | `/accounts` | Accounts | Qoder login + pool | | `/providers` | Models | Catalog + per-model context-window defaults | | `/access` | Access | Base URL + quick chat | @@ -447,6 +447,7 @@ Rules: - Runtime ring redacts obvious secrets and is lost on restart. Docker compose logs remain the durable operator stream for first-boot API key recovery. - `/api/logs/*` requires the SQLite API key. - Request history list accepts `account`, `status`, `stream`, `error_kind`, `model`, `q`, `from`, `to`, `limit`, and `offset`. The console `/logs` page paginates this list and exposes those filters. +- `GET /api/logs/stats` aggregates counts, success rate, latency percentiles, tokens, error mix, and a time series for Overview. Windows are 1h / 24h / 7d; series buckets are 15 minutes, hourly, or daily. - Runtime snapshot accepts `account` in addition to `level` and `q`. ## Managed update diff --git a/frontend/src/api/logs.ts b/frontend/src/api/logs.ts index 40520934..f40a8c18 100644 --- a/frontend/src/api/logs.ts +++ b/frontend/src/api/logs.ts @@ -62,6 +62,67 @@ export type RuntimeLogList = { count: number } +export type RequestStatsWindow = { + from: string + to: string + hours: number +} + +export type RequestStatsTotals = { + requests: number + ok: number + error: number + canceled: number + streaming: number + success_rate: number +} + +export type RequestStatsLatency = { + avg_ms?: number | null + p50_ms?: number | null + p95_ms?: number | null + ttfb_avg_ms?: number | null +} + +export type RequestStatsTokens = { + prompt: number + completion: number + cache_read: number + total: number +} + +export type RequestStatsBucket = { + key: string + count: number +} + +export type RequestStatsNamed = { + key: string + count: number + ok: number + error: number + latency_avg_ms?: number | null +} + +export type RequestStatsPoint = { + at: string + requests: number + ok: number + error: number +} + +export type RequestStats = { + window: RequestStatsWindow + totals: RequestStatsTotals + latency: RequestStatsLatency + tokens: RequestStatsTokens + status: RequestStatsBucket[] + errors: RequestStatsBucket[] + models: RequestStatsNamed[] + accounts: RequestStatsNamed[] + series: RequestStatsPoint[] +} + export type RequestLogQuery = { account?: string status?: string @@ -75,6 +136,15 @@ export type RequestLogQuery = { offset?: number } +export function fetchRequestStats(query: { hours?: number; from?: string; to?: string } = {}) { + const params = new URLSearchParams() + if (query.hours != null) params.set('hours', String(query.hours)) + if (query.from) params.set('from', query.from) + if (query.to) params.set('to', query.to) + const suffix = params.toString() ? `?${params}` : '' + return api(`/api/logs/stats${suffix}`) +} + export function fetchRequestLogs(query: RequestLogQuery = {}) { const params = new URLSearchParams() if (query.account) params.set('account', query.account) diff --git a/frontend/src/components/overview/CountUp.tsx b/frontend/src/components/overview/CountUp.tsx new file mode 100644 index 00000000..9a1eab9a --- /dev/null +++ b/frontend/src/components/overview/CountUp.tsx @@ -0,0 +1,40 @@ +import { useLayoutEffect, useRef } from 'react' +import gsap from 'gsap' +import { formatCountKind } from '@/lib/format' + +export function CountUp({ + value, + kind = 'int', + className, +}: { + value: number + kind?: 'int' | 'compact' | 'percent' | 'ms' + className?: string +}) { + const ref = useRef(null) + + useLayoutEffect(() => { + const el = ref.current + if (!el) return + const state = { value: 0 } + const context = gsap.context(() => { + const media = gsap.matchMedia() + media.add('(prefers-reduced-motion: reduce)', () => { + el.textContent = formatCountKind(value, kind) + }) + media.add('(prefers-reduced-motion: no-preference)', () => { + gsap.to(state, { + value, + duration: 0.5, + ease: 'power3.out', + onUpdate: () => { + el.textContent = formatCountKind(state.value, kind) + }, + }) + }) + }, el) + return () => context.revert() + }, [kind, value]) + + return {formatCountKind(0, kind)} +} diff --git a/frontend/src/components/overview/TrafficChart.tsx b/frontend/src/components/overview/TrafficChart.tsx new file mode 100644 index 00000000..def652a2 --- /dev/null +++ b/frontend/src/components/overview/TrafficChart.tsx @@ -0,0 +1,87 @@ +import { useLayoutEffect, useRef } from 'react' +import gsap from 'gsap' +import type { RequestStatsPoint } from '@/api/logs' + +function hourLabel(value: string, daily: boolean, lang: 'en' | 'zh') { + const date = new Date(value) + if (!Number.isFinite(date.getTime())) return '' + return new Intl.DateTimeFormat(lang === 'zh' ? 'zh-CN' : 'en-US', daily + ? { month: 'short', day: 'numeric' } + : { hour: '2-digit', minute: '2-digit', hour12: false }).format(date) +} + +export function TrafficChart({ + series, + lang, + emptyLabel, +}: { + series: RequestStatsPoint[] + lang: 'en' | 'zh' + emptyLabel: string +}) { + const rootRef = useRef(null) + const peak = Math.max(1, ...series.map((point) => point.requests)) + const first = series[0] + const last = series[series.length - 1] + const span = first && last ? Date.parse(last.at) - Date.parse(first.at) : 0 + const daily = span > 48 * 60 * 60 * 1000 + + useLayoutEffect(() => { + const root = rootRef.current + if (!root) return + const context = gsap.context(() => { + const media = gsap.matchMedia() + media.add('(prefers-reduced-motion: reduce)', () => { + gsap.set('[data-bar]', { clearProps: 'transform' }) + }) + media.add('(prefers-reduced-motion: no-preference)', () => { + const bars = gsap.utils.toArray('[data-bar]') + if (!bars.length) return + gsap.fromTo(bars, { scaleY: 0 }, { + scaleY: 1, + duration: 0.42, + ease: 'power2.out', + stagger: { amount: Math.min(0.45, bars.length * 0.012) }, + transformOrigin: '50% 100%', + }) + }) + }, root) + return () => context.revert() + }, [series]) + + if (!series.length || series.every((point) => point.requests === 0)) { + return ( +
+ {emptyLabel} +
+ ) + } + + return ( +
+
+ {series.map((point) => { + const height = Math.max(point.requests ? 8 : 2, Math.round((point.requests / peak) * 100)) + const failed = point.requests > 0 && point.error / point.requests >= 0.5 + return ( +
+ +
+ ) + })} +
+
+ {first ? hourLabel(first.at, daily, lang) : ''} + {last ? hourLabel(last.at, daily, lang) : ''} +
+
+ ) +} diff --git a/frontend/src/components/ui/PageSkeletons.tsx b/frontend/src/components/ui/PageSkeletons.tsx index 297ee414..317b6e3a 100644 --- a/frontend/src/components/ui/PageSkeletons.tsx +++ b/frontend/src/components/ui/PageSkeletons.tsx @@ -6,10 +6,11 @@ function SkeletonBlock({ className }: { className: string }) { export function OverviewPageSkeleton() { return ( -
-
-
{Array.from({ length: 4 }, (_, index) =>
)}
-
{Array.from({ length: 5 }, (_, index) => )}
{Array.from({ length: 4 }, (_, index) => )}
+
+
+
{Array.from({ length: 4 }, (_, index) =>
)}
+
{Array.from({ length: 4 }, (_, index) => )}
{Array.from({ length: 5 }, (_, index) => )}
+
{Array.from({ length: 3 }, (_, index) =>
{Array.from({ length: 4 }, (_, item) => )}
)}
) } diff --git a/frontend/src/i18n/messages.ts b/frontend/src/i18n/messages.ts index bb5077e4..66df7082 100644 --- a/frontend/src/i18n/messages.ts +++ b/frontend/src/i18n/messages.ts @@ -96,7 +96,7 @@ export const messages: Record = { checkingSession: 'Checking session…', refreshing: 'Refreshing', pages: { - home: { title: 'Overview', desc: 'Inspect local proxy health and ready endpoints.' }, + home: { title: 'Overview', desc: 'Runtime health, request volume, and ready endpoints.' }, auth: { title: 'Auth', desc: 'Browser device-flow login, PAT login, and session rewarm.' }, providers: { title: 'Models', desc: 'Models currently exposed by signed-in accounts.' }, access: { title: 'Access', desc: 'Copy Base URL and run a quick Chat Completions check.' }, @@ -164,7 +164,35 @@ export const messages: Record = { homeEyebrow: 'Runtime', homeDisplay: 'Gateway pulse', - homeLead: 'Dense status without boxed metric cards. Quiet chrome, cream actions, green only for health.', + homeLead: 'Runtime health plus request volume, latency, and tokens from this host.', + metricRequests: 'Requests', + metricSuccess: 'Success', + metricLatency: 'p95', + metricTokens: 'Tokens', + statsWindow1h: '1h', + statsWindow24h: '24h', + statsWindow7d: '7d', + statsWindowHint: '{window} window', + statsTraffic: 'Traffic', + statsTrafficHint: 'Completed chat requests in the selected window', + statsEmptyTraffic: 'No requests in this window yet.', + statsLatencyP50: 'p50', + statsLatencyP95: 'p95', + statsLatencyAvg: 'avg', + statsTTFB: 'TTFB', + statsPool: 'Account pool', + statsPoolHint: 'Ready workers and remaining quota', + statsCooling: 'cooling', + statsTopModels: 'Top models', + statsTopAccounts: 'Top accounts', + statsErrorMix: 'Error mix', + statsErrorMixHint: 'Classified failures in this window', + statsNoModels: 'No model traffic yet.', + statsNoAccounts: 'No account traffic yet.', + statsNoErrors: 'No classified errors.', + statsUnknown: 'unknown', + statsUnassigned: 'unassigned', + failedStats: 'Failed to load stats: {msg}', homeNote: 'Signed into the local console. Account logins live on the Accounts page.', metricRuntime: 'Runtime', metricAccounts: 'Accounts', @@ -469,7 +497,7 @@ export const messages: Record = { checkingSession: '正在检查登录态…', refreshing: '刷新中', pages: { - home: { title: '概览', desc: '查看本地代理运行状态与可用端点。' }, + home: { title: '概览', desc: '查看运行状态、请求量与可用端点。' }, auth: { title: '授权', desc: '浏览器 device-flow 登录、PAT 登录,以及会话 rewarm。' }, providers: { title: '模型', desc: '当前已登录账号可用的模型列表。' }, access: { title: '接入', desc: '复制 Base URL,并快速验证 Chat Completions。' }, @@ -537,7 +565,35 @@ export const messages: Record = { homeEyebrow: '运行时', homeDisplay: '网关状态', - homeLead: '信息密集但不堆卡片。主操作奶油色,绿色只表示健康状态。', + homeLead: '运行状态之外,还能看到本机请求量、延迟和 token。', + metricRequests: '请求', + metricSuccess: '成功率', + metricLatency: 'p95', + metricTokens: 'Tokens', + statsWindow1h: '1小时', + statsWindow24h: '24小时', + statsWindow7d: '7天', + statsWindowHint: '{window}窗口', + statsTraffic: '流量', + statsTrafficHint: '所选时间窗口内的已完成聊天请求', + statsEmptyTraffic: '这个时间窗口还没有请求。', + statsLatencyP50: 'p50', + statsLatencyP95: 'p95', + statsLatencyAvg: '平均', + statsTTFB: 'TTFB', + statsPool: '账号池', + statsPoolHint: '就绪 worker 与剩余额度', + statsCooling: '冷却中', + statsTopModels: '热门模型', + statsTopAccounts: '热门账号', + statsErrorMix: '错误构成', + statsErrorMixHint: '该窗口内已分类的失败', + statsNoModels: '还没有模型流量。', + statsNoAccounts: '还没有账号流量。', + statsNoErrors: '没有分类错误。', + statsUnknown: '未知', + statsUnassigned: '未分配', + failedStats: '加载统计失败:{msg}', homeNote: '已登录本机控制台。账号登录在「账号」页。', metricRuntime: '运行状态', metricAccounts: '账号', diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts new file mode 100644 index 00000000..812391bb --- /dev/null +++ b/frontend/src/lib/format.ts @@ -0,0 +1,31 @@ +export function formatCompact(value: number) { + if (!Number.isFinite(value)) return '—' + const abs = Math.abs(value) + if (abs >= 1_000_000) return `${trimFixed(value / 1_000_000)}M` + if (abs >= 1000) return `${trimFixed(value / 1000)}k` + return String(Math.round(value)) +} + +export function formatLatency(ms: number | null | undefined) { + if (ms == null || !Number.isFinite(ms)) return '—' + if (ms >= 1000) return `${trimFixed(ms / 1000)}s` + return `${Math.round(ms)}ms` +} + +export function formatPercent(rate: number | null | undefined) { + if (rate == null || !Number.isFinite(rate)) return '—' + const pct = rate * 100 + return `${pct >= 10 ? pct.toFixed(0) : pct.toFixed(1)}%` +} + +export function formatCountKind(value: number, kind: 'int' | 'compact' | 'percent' | 'ms') { + if (kind === 'compact') return formatCompact(value) + if (kind === 'percent') return formatPercent(value) + if (kind === 'ms') return formatLatency(value) + return String(Math.round(value)) +} + +function trimFixed(value: number) { + const digits = Math.abs(value) >= 10 ? 0 : 1 + return Number(value.toFixed(digits)).toString() +} diff --git a/frontend/src/pages/OverviewPage.tsx b/frontend/src/pages/OverviewPage.tsx index 57e5f94b..a0d8b0bd 100644 --- a/frontend/src/pages/OverviewPage.tsx +++ b/frontend/src/pages/OverviewPage.tsx @@ -1,109 +1,329 @@ -import { useState } from 'react' -import { Button, Card, Chip, Skeleton } from '@heroui/react' -import { ArrowUpRight, Cube, Copy, Database, ArrowSquareOut, Desktop, UsersThree } from '@phosphor-icons/react' +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import gsap from 'gsap' +import { Link } from 'react-router-dom' +import { Button, ButtonGroup, Card, Chip } from '@heroui/react' +import { + ArrowSquareOut, + ArrowUpRight, + Copy, + Cube, + Pulse, + WarningCircle, +} from '@phosphor-icons/react' +import { fetchRequestStats, type RequestStats } from '@/api/logs' +import type { Overview } from '@/api/types' +import { CountUp } from '@/components/overview/CountUp' +import { TrafficChart } from '@/components/overview/TrafficChart' +import { OverviewPageSkeleton } from '@/components/ui/PageSkeletons' import { useI18n } from '@/hooks/useI18n' import { useOverview } from '@/hooks/useOverview' +import { formatCompact, formatLatency, formatPercent } from '@/lib/format' import { absUrl } from '@/lib/url' -import { OverviewPageSkeleton } from '@/components/ui/PageSkeletons' + +type AccountRow = NonNullable[number] +type StatsWindow = 1 | 24 | 168 + +const EMPTY_STATS: RequestStats = { + window: { from: '', to: '', hours: 24 }, + totals: { requests: 0, ok: 0, error: 0, canceled: 0, streaming: 0, success_rate: 0 }, + latency: {}, + tokens: { prompt: 0, completion: 0, cache_read: 0, total: 0 }, + status: [], + errors: [], + models: [], + accounts: [], + series: [], +} + +function accountLabel(accounts: AccountRow[], id: string, unassigned: string) { + if (!id || id === '(unassigned)') return unassigned + return accounts.find((account) => account.id === id)?.name || id +} + +function errorLabel(kind: string, t: (key: string) => string) { + const keys: Record = { + quota: 'logsKindQuota', + rate_limit: 'logsKindRateLimit', + auth: 'logsKindAuth', + not_ready: 'logsKindNotReady', + unavailable: 'logsKindUnavailable', + invalid_request: 'logsKindInvalidRequest', + model_not_available: 'logsKindModelNotAvailable', + } + return keys[kind] ? t(keys[kind]) : kind +} + +function quotaTone(percentage?: number, exceeded?: boolean) { + if (exceeded || (percentage ?? 0) >= 100) return 'danger' + if ((percentage ?? 0) >= 80) return 'warning' + return 'ok' +} export function OverviewPage() { - const { t } = useI18n() + const { lang, t } = useI18n() const { overview, loading, error } = useOverview() + const [hours, setHours] = useState(24) + const [stats, setStats] = useState(null) + const [statsError, setStatsError] = useState('') + const [copiedEndpoint, setCopiedEndpoint] = useState('') + const [statsLoading, setStatsLoading] = useState(true) + const proxyOk = Boolean(overview?.proxy?.ok) const workerOk = Boolean(overview?.worker?.ok) - const hot = Boolean(overview?.worker?.hot) const accounts = overview?.accounts || [] const readyAccounts = accounts.filter((account) => account.ready).length const hotAccounts = accounts.filter((account) => account.hot).length + const coolingAccounts = accounts.filter((account) => account.down_until || account.cooldown_until).length + const inFlight = accounts.reduce((total, account) => total + (account.in_flight ?? account.inFlight ?? 0), 0) const modelCount = overview?.models?.length ?? 0 + const lastError = accounts.map((account) => account.last_error || account.lastError).find(Boolean) || '—' + const traffic = stats ?? EMPTY_STATS const base = absUrl(overview?.access?.openai_base_url || '/v1') - const [copiedEndpoint, setCopiedEndpoint] = useState('') - if (loading) return + useEffect(() => { + let cancelled = false + void fetchRequestStats({ hours }) + .then((data) => { + if (cancelled) return + setStats(data) + setStatsError('') + }) + .catch((err) => { + if (cancelled) return + setStats(null) + setStatsError(err instanceof Error ? err.message : String(err)) + }) + .finally(() => { + if (!cancelled) setStatsLoading(false) + }) + return () => { + cancelled = true + } + }, [hours]) - const metrics = [ - { label: t('metricRuntime'), value: proxyOk ? t('running') : t('down'), detail: 'Go control plane', icon: Desktop, ok: proxyOk }, - { label: t('metricAccounts'), value: String(accounts.length), detail: `${hotAccounts} hot · ${readyAccounts} ready`, icon: UsersThree, ok: readyAccounts > 0 }, - { label: t('metricModels'), value: String(modelCount), detail: modelCount ? t('providerCatalog') : '—', icon: Cube, ok: modelCount > 0 }, - { label: 'SQLite', value: accounts.length ? t('ready') : t('checking'), detail: '/data', icon: Database, ok: accounts.length > 0 }, - ] - - const endpoints = [ + const endpoints = useMemo(() => [ { name: t('endpointOpenAI'), url: base, method: 'BASE' }, { name: t('endpointChat'), url: absUrl(overview?.access?.chat_completions || `${base}/chat/completions`), method: 'POST' }, { name: t('endpointModels'), url: absUrl(overview?.access?.models || `${base}/models`), method: 'GET' }, { name: t('endpointHealth'), url: absUrl(overview?.access?.health || '/health'), method: 'GET' }, + ], [base, overview, t]) + + const metrics = [ + { label: t('metricRequests'), value: traffic.totals.requests as number | null, kind: 'compact' as const, detail: t('statsWindowHint', { window: t(hours === 1 ? 'statsWindow1h' : hours === 168 ? 'statsWindow7d' : 'statsWindow24h') }), ok: traffic.totals.requests > 0 }, + { label: t('metricSuccess'), value: traffic.totals.success_rate, kind: 'percent' as const, detail: `${traffic.totals.ok} ${t('logsFilterOk')} · ${traffic.totals.error} ${t('logsFilterError')}`, ok: traffic.totals.requests === 0 || traffic.totals.success_rate >= 0.9 }, + { label: t('metricLatency'), value: traffic.latency.p95_ms ?? traffic.latency.avg_ms, kind: 'ms' as const, detail: `p50 ${formatLatency(traffic.latency.p50_ms)} · avg ${formatLatency(traffic.latency.avg_ms)}`, ok: traffic.latency.p95_ms == null || traffic.latency.p95_ms < 8000 }, + { label: t('metricTokens'), value: traffic.tokens.total, kind: 'compact' as const, detail: `${formatCompact(traffic.tokens.prompt)} / ${formatCompact(traffic.tokens.completion)}`, ok: true }, ] + if (loading) return + return (
-
+

{t('homeDisplay')}

{t('homeLead')}

-
-
{overview?.time || '—'}
-
- - {proxyOk && workerOk ? t('running') : t('degraded')} +
+ + {([1, 24, 168] as const).map((value) => ( + + ))} + +
+
{overview?.time || '—'}
+
+ + {proxyOk && workerOk ? t('running') : t('degraded')} +
+ {error ? ( +
+ + {t('failedOverview', { msg: error })} +
+ ) : null} +
- {metrics.map((metric, index) => { - const Icon = metric.icon - return ( -
-
- {metric.label} - -
-
-
-
{loading ? '…' : metric.value}
-
{metric.detail}
+ {metrics.map((metric, index) => ( +
+
{metric.label}
+
+
+
+ {statsLoading && !stats ? '—' : metric.value == null ? '—' : }
- +
{metric.detail}
+
- ) - })} +
+ ))}
-
+
-

{t('serviceDetail')}

-

{t('runtimeSnapshot')}

+

{t('statsTraffic')}

+

{t('statsTrafficHint')}

- {workerOk ? t('ready') : t('degraded')} + + {formatPercent(traffic.totals.success_rate)} +
- {loading ? ( -
- - - -
- ) : error ? ( -
{t('failedOverview', { msg: error })}
- ) : ( -
+
+ {statsError ? ( +
{t('failedStats', { msg: statsError })}
+ ) : ( + + )} +
{[ - [t('proxy'), proxyOk ? 'online' : t('down')], - [t('sessionHot'), hot ? 'true' : 'false'], - [t('upstreamEndpoint'), overview?.worker?.endpoint || overview?.proxy?.chat_url || '—'], - [t('rewarm'), String(overview?.worker?.rewarmCount ?? overview?.worker?.rewarm_count ?? 0)], - [t('lastError'), overview?.worker?.lastError || overview?.worker?.last_error || '—'], + [t('statsLatencyP50'), formatLatency(traffic.latency.p50_ms)], + [t('statsLatencyP95'), formatLatency(traffic.latency.p95_ms)], + [t('statsLatencyAvg'), formatLatency(traffic.latency.avg_ms)], + [t('statsTTFB'), formatLatency(traffic.latency.ttfb_avg_ms)], ].map(([label, value]) => ( -
-
{label}
-
{value}
+
+
{label}
+
{value}
))} -
+
+
+ + + +
+
+

{t('statsPool')}

+

{t('statsPoolHint')}

+
+ + {t('navAccounts')} + + +
+
+ {[ + [t('ready'), readyAccounts, 'ok'], + [t('hot'), hotAccounts, hotAccounts ? 'ok' : ''], + [t('inFlight'), inFlight, inFlight ? 'ok' : ''], + ].map(([label, value, state]) => ( +
+
{label}
+
+ {value} + {state ? : null} +
+
+ ))} +
+
+ {accounts.length === 0 ? ( +
{t('noAccounts')}
+ ) : accounts.slice(0, 6).map((account) => { + const quota = account.quota + const tone = quotaTone(quota?.percentage, quota?.exceeded) + return ( +
+ +
+
{account.name || account.id}
+
+ {account.hot ? t('hot') : account.ready ? t('ready') : t('degraded')} + {quota ? ` · ${Math.round(quota.percentage ?? 0)}%` : ''} +
+
+ {quota ? ( +
+
+
+
+
+ ) : ( + {account.in_flight ?? account.inFlight ?? 0} + )} +
+ ) + })} +
+
+ {t('metricModels')} {modelCount} + {coolingAccounts ? `${coolingAccounts} ${t('statsCooling')}` : `${accounts.length} ${t('accountCount')}`} +
+ + + +
+ +
+
+

{t('statsTopModels')}

+

{t('providerCatalog')}

+
+ +
+ ({ + key: item.key, + label: item.key === '(unknown)' ? t('statsUnknown') : item.key, + count: item.count, + meta: item.latency_avg_ms != null ? formatLatency(item.latency_avg_ms) : `${item.ok}/${item.count}`, + }))} + /> +
+ + +
+
+

{traffic.errors.length ? t('statsErrorMix') : t('statsTopAccounts')}

+

{traffic.errors.length ? t('statsErrorMixHint') : t('statsPoolHint')}

+
+ +
+ {traffic.errors.length ? ( + ({ + key: item.key, + label: errorLabel(item.key, t), + count: item.count, + }))} + /> + ) : ( + ({ + key: item.key, + label: accountLabel(accounts, item.key, t('statsUnassigned')), + count: item.count, + meta: `${item.ok}/${item.count}`, + }))} + /> )}
@@ -114,7 +334,7 @@ export function OverviewPage() {
{endpoints.map((item) => ( -
+
{item.method}
{item.name}
@@ -128,11 +348,73 @@ export function OverviewPage() { ))}
- Authorization: Bearer - + {lastError === '—' ? 'Authorization: Bearer' : lastError} + {t('navLogs')}
) } + +function RankList({ + items, + empty, + danger, +}: { + items: Array<{ key: string; label: string; count: number; meta?: string }> + empty: string + danger?: boolean +}) { + const rootRef = useRef(null) + const peak = Math.max(1, ...items.map((item) => item.count)) + const signature = items.map((item) => `${item.key}:${item.count}`).join('|') + + useLayoutEffect(() => { + const root = rootRef.current + if (!root) return + const context = gsap.context(() => { + const media = gsap.matchMedia() + media.add('(prefers-reduced-motion: reduce)', () => { + gsap.set('[data-rank-fill]', { clearProps: 'transform' }) + }) + media.add('(prefers-reduced-motion: no-preference)', () => { + const fills = gsap.utils.toArray('[data-rank-fill]') + if (!fills.length) return + gsap.fromTo(fills, { scaleX: 0 }, { + scaleX: 1, + duration: 0.38, + ease: 'power2.out', + stagger: 0.05, + transformOrigin: '0% 50%', + }) + }) + }, root) + return () => context.revert() + }, [signature]) + + if (!items.length) { + return
{empty}
+ } + return ( +
+ {items.map((item) => ( +
+
+
{item.label}
+
+ {item.meta ? `${item.count} · ${item.meta}` : item.count} +
+
+
+
+
+
+ ))} +
+ ) +} diff --git a/internal/accounts/request_logs.go b/internal/accounts/request_logs.go index 36169eb0..1be6336a 100644 --- a/internal/accounts/request_logs.go +++ b/internal/accounts/request_logs.go @@ -87,6 +87,67 @@ type RequestLogList struct { Offset int `json:"offset"` } +type RequestStats struct { + Window RequestStatsWindow `json:"window"` + Totals RequestStatsTotals `json:"totals"` + Latency RequestStatsLatency `json:"latency"` + Tokens RequestStatsTokens `json:"tokens"` + Status []RequestStatsBucket `json:"status"` + Errors []RequestStatsBucket `json:"errors"` + Models []RequestStatsNamed `json:"models"` + Accounts []RequestStatsNamed `json:"accounts"` + Series []RequestStatsPoint `json:"series"` +} + +type RequestStatsWindow struct { + From time.Time `json:"from"` + To time.Time `json:"to"` + Hours int `json:"hours"` +} + +type RequestStatsTotals struct { + Requests int `json:"requests"` + OK int `json:"ok"` + Error int `json:"error"` + Canceled int `json:"canceled"` + Streaming int `json:"streaming"` + SuccessRate float64 `json:"success_rate"` +} + +type RequestStatsLatency struct { + AvgMs *int `json:"avg_ms,omitempty"` + P50Ms *int `json:"p50_ms,omitempty"` + P95Ms *int `json:"p95_ms,omitempty"` + TTFBAvgMs *int `json:"ttfb_avg_ms,omitempty"` +} + +type RequestStatsTokens struct { + Prompt int64 `json:"prompt"` + Completion int64 `json:"completion"` + CacheRead int64 `json:"cache_read"` + Total int64 `json:"total"` +} + +type RequestStatsBucket struct { + Key string `json:"key"` + Count int `json:"count"` +} + +type RequestStatsNamed struct { + Key string `json:"key"` + Count int `json:"count"` + OK int `json:"ok"` + Error int `json:"error"` + LatencyAvgMs *int `json:"latency_avg_ms,omitempty"` +} + +type RequestStatsPoint struct { + At time.Time `json:"at"` + Requests int `json:"requests"` + OK int `json:"ok"` + Error int `json:"error"` +} + func NewRequestID() string { raw := make([]byte, 8) if _, err := rand.Read(raw); err != nil { @@ -240,6 +301,300 @@ FROM request_logs` + where + ` ORDER BY created_at DESC, id DESC LIMIT ? OFFSET return RequestLogList{Items: items, Total: total, Limit: limit, Offset: offset}, nil } +func (s *Store) SummarizeRequestLogs(ctx context.Context, from, to time.Time) (RequestStats, error) { + if from.IsZero() { + from = time.Now().UTC().Add(-24 * time.Hour) + } + if to.IsZero() { + to = time.Now().UTC() + } + from = from.UTC() + to = to.UTC() + if !to.After(from) { + to = from.Add(time.Hour) + } + hours := int(to.Sub(from).Round(time.Hour) / time.Hour) + if hours < 1 { + hours = 1 + } + + filter := RequestLogFilter{From: &from, To: &to} + where, args := buildRequestLogWhere(filter) + stats := RequestStats{ + Window: RequestStatsWindow{From: from, To: to, Hours: hours}, + Status: make([]RequestStatsBucket, 0), + Errors: make([]RequestStatsBucket, 0), + Models: make([]RequestStatsNamed, 0), + Accounts: make([]RequestStatsNamed, 0), + Series: make([]RequestStatsPoint, 0, hours), + } + + row := s.db.QueryRowContext(ctx, ` +SELECT + COUNT(*), + COALESCE(SUM(CASE WHEN status = 'ok' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN status = 'canceled' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN stream = 1 THEN 1 ELSE 0 END), 0), + AVG(CASE WHEN latency_ms IS NOT NULL THEN latency_ms END), + AVG(CASE WHEN ttfb_ms IS NOT NULL THEN ttfb_ms END), + COALESCE(SUM(COALESCE(prompt_tokens, 0)), 0), + COALESCE(SUM(COALESCE(completion_tokens, 0)), 0), + COALESCE(SUM(COALESCE(cache_read_tokens, 0)), 0) +FROM request_logs`+where, args...) + var avgLatency, avgTTFB sql.NullFloat64 + if err := row.Scan( + &stats.Totals.Requests, &stats.Totals.OK, &stats.Totals.Error, &stats.Totals.Canceled, &stats.Totals.Streaming, + &avgLatency, &avgTTFB, &stats.Tokens.Prompt, &stats.Tokens.Completion, &stats.Tokens.CacheRead, + ); err != nil { + return RequestStats{}, fmt.Errorf("summarize request logs: %w", err) + } + stats.Tokens.Total = stats.Tokens.Prompt + stats.Tokens.Completion + if stats.Totals.Requests > 0 { + stats.Totals.SuccessRate = float64(stats.Totals.OK) / float64(stats.Totals.Requests) + } + stats.Latency.AvgMs = roundedNullInt(avgLatency) + stats.Latency.TTFBAvgMs = roundedNullInt(avgTTFB) + stats.Latency.P50Ms, stats.Latency.P95Ms = s.requestLatencyPercentiles(ctx, where, args) + + statusRows, err := s.db.QueryContext(ctx, ` +SELECT status, COUNT(*) FROM request_logs`+where+` GROUP BY status ORDER BY COUNT(*) DESC, status ASC`, args...) + if err != nil { + return RequestStats{}, fmt.Errorf("summarize request status: %w", err) + } + stats.Status, err = scanCountBuckets(statusRows) + if err != nil { + return RequestStats{}, err + } + + errorRows, err := s.db.QueryContext(ctx, ` +SELECT error_kind, COUNT(*) FROM request_logs`+whereAnd(where, "error_kind <> ''")+` +GROUP BY error_kind ORDER BY COUNT(*) DESC, error_kind ASC LIMIT 8`, args...) + if err != nil { + return RequestStats{}, fmt.Errorf("summarize request errors: %w", err) + } + stats.Errors, err = scanCountBuckets(errorRows) + if err != nil { + return RequestStats{}, err + } + + modelRows, err := s.db.QueryContext(ctx, ` +SELECT COALESCE(NULLIF(requested_model, ''), '(unknown)'), COUNT(*), + COALESCE(SUM(CASE WHEN status = 'ok' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0), + AVG(CASE WHEN latency_ms IS NOT NULL THEN latency_ms END) +FROM request_logs`+where+` GROUP BY 1 ORDER BY COUNT(*) DESC, 1 ASC LIMIT 6`, args...) + if err != nil { + return RequestStats{}, fmt.Errorf("summarize request models: %w", err) + } + stats.Models, err = scanNamedBuckets(modelRows) + if err != nil { + return RequestStats{}, err + } + + accountRows, err := s.db.QueryContext(ctx, ` +SELECT COALESCE(NULLIF(account_id, ''), '(unassigned)'), COUNT(*), + COALESCE(SUM(CASE WHEN status = 'ok' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0), + AVG(CASE WHEN latency_ms IS NOT NULL THEN latency_ms END) +FROM request_logs`+where+` GROUP BY 1 ORDER BY COUNT(*) DESC, 1 ASC LIMIT 6`, args...) + if err != nil { + return RequestStats{}, fmt.Errorf("summarize request accounts: %w", err) + } + stats.Accounts, err = scanNamedBuckets(accountRows) + if err != nil { + return RequestStats{}, err + } + + series, err := s.requestSeries(ctx, from, to, where, args) + if err != nil { + return RequestStats{}, err + } + stats.Series = series + return stats, nil +} + +func (s *Store) requestLatencyPercentiles(ctx context.Context, where string, args []any) (*int, *int) { + rows, err := s.db.QueryContext(ctx, `SELECT latency_ms FROM request_logs`+whereAnd(where, "latency_ms IS NOT NULL")+` ORDER BY latency_ms ASC`, args...) + if err != nil { + return nil, nil + } + defer rows.Close() + values := make([]int, 0, 128) + for rows.Next() { + var value int + if err := rows.Scan(&value); err != nil { + return nil, nil + } + values = append(values, value) + } + if len(values) == 0 { + return nil, nil + } + return roundedInt(percentileNearestRank(values, 50)), roundedInt(percentileNearestRank(values, 95)) +} + +func (s *Store) requestSeries(ctx context.Context, from, to time.Time, where string, args []any) ([]RequestStatsPoint, error) { + span := to.Sub(from) + daily := span > 48*time.Hour + quarter := !daily && span <= 2*time.Hour + step := time.Hour + trunc := from.Truncate(time.Hour) + end := to.Truncate(time.Hour) + if daily { + step = 24 * time.Hour + trunc = time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, time.UTC) + end = time.Date(to.Year(), to.Month(), to.Day(), 0, 0, 0, 0, time.UTC) + } else if quarter { + step = 15 * time.Minute + trunc = from.Truncate(15 * time.Minute) + end = to.Truncate(15 * time.Minute) + } + if to.After(end) { + end = end.Add(step) + } + points := make([]RequestStatsPoint, 0) + index := map[string]int{} + for cursor := trunc; cursor.Before(end); cursor = cursor.Add(step) { + index[seriesKey(cursor, daily, quarter)] = len(points) + points = append(points, RequestStatsPoint{At: cursor.UTC()}) + } + if len(points) == 0 { + return points, nil + } + + expr := "substr(created_at, 1, 13)" + if daily { + expr = "substr(created_at, 1, 10)" + } else if quarter { + expr = "substr(created_at, 1, 14) || printf('%02d', (CAST(substr(created_at, 15, 2) AS INTEGER) / 15) * 15)" + } + rows, err := s.db.QueryContext(ctx, fmt.Sprintf(` +SELECT %s, COUNT(*), + COALESCE(SUM(CASE WHEN status = 'ok' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0) +FROM request_logs%s GROUP BY 1 ORDER BY 1 ASC`, expr, where), args...) + if err != nil { + return nil, fmt.Errorf("summarize request series: %w", err) + } + defer rows.Close() + for rows.Next() { + var bucket string + var requests, okCount, errorCount int + if err := rows.Scan(&bucket, &requests, &okCount, &errorCount); err != nil { + return nil, fmt.Errorf("scan request series: %w", err) + } + if i, exists := index[normalizeSeriesBucket(bucket, daily, quarter)]; exists { + points[i].Requests = requests + points[i].OK = okCount + points[i].Error = errorCount + } + } + return points, rows.Err() +} + +func seriesKey(value time.Time, daily, quarter bool) string { + utc := value.UTC() + if daily { + return utc.Format("2006-01-02") + } + if quarter { + return utc.Truncate(15 * time.Minute).Format("2006-01-02T15:04") + } + return utc.Format("2006-01-02T15") +} + +func normalizeSeriesBucket(bucket string, daily, quarter bool) string { + text := strings.TrimSpace(bucket) + if daily { + if len(text) >= 10 { + return text[:10] + } + return text + } + if quarter { + if len(text) >= 16 { + parsed, err := time.Parse("2006-01-02T15:04", text[:16]) + if err == nil { + return parsed.UTC().Truncate(15 * time.Minute).Format("2006-01-02T15:04") + } + return text[:16] + } + return text + } + if len(text) >= 13 { + return text[:13] + } + return text +} + +func scanCountBuckets(rows *sql.Rows) ([]RequestStatsBucket, error) { + defer rows.Close() + items := make([]RequestStatsBucket, 0) + for rows.Next() { + var item RequestStatsBucket + if err := rows.Scan(&item.Key, &item.Count); err != nil { + return nil, fmt.Errorf("scan request bucket: %w", err) + } + if strings.TrimSpace(item.Key) == "" { + continue + } + items = append(items, item) + } + return items, rows.Err() +} + +func scanNamedBuckets(rows *sql.Rows) ([]RequestStatsNamed, error) { + defer rows.Close() + items := make([]RequestStatsNamed, 0) + for rows.Next() { + var item RequestStatsNamed + var avg sql.NullFloat64 + if err := rows.Scan(&item.Key, &item.Count, &item.OK, &item.Error, &avg); err != nil { + return nil, fmt.Errorf("scan named request bucket: %w", err) + } + item.LatencyAvgMs = roundedNullInt(avg) + items = append(items, item) + } + return items, rows.Err() +} + +func whereAnd(where, extra string) string { + if strings.TrimSpace(where) == "" { + return " WHERE " + extra + } + return where + " AND " + extra +} + +func roundedNullInt(value sql.NullFloat64) *int { + if !value.Valid { + return nil + } + rounded := int(value.Float64 + 0.5) + return &rounded +} + +func roundedInt(value int) *int { + return &value +} + +func percentileNearestRank(sorted []int, percentile int) int { + if len(sorted) == 0 { + return 0 + } + if percentile <= 0 { + return sorted[0] + } + if percentile >= 100 { + return sorted[len(sorted)-1] + } + rank := int((float64(percentile) / 100) * float64(len(sorted))) + if rank >= len(sorted) { + rank = len(sorted) - 1 + } + return sorted[rank] +} + func (s *Store) GetRequestLog(ctx context.Context, id string) (RequestLog, error) { row := s.db.QueryRowContext(ctx, ` SELECT id, created_at, finished_at, stream, status, requested_model, mapped_model, account_id, diff --git a/internal/accounts/request_logs_test.go b/internal/accounts/request_logs_test.go index 119dea69..4e244ae1 100644 --- a/internal/accounts/request_logs_test.go +++ b/internal/accounts/request_logs_test.go @@ -187,3 +187,99 @@ func TestRequestLogsCapPurgeKeepsNewest(t *testing.T) { t.Fatalf("expected 3 rows after cap purge, got %d", list.Total) } } + +func TestSummarizeRequestLogs(t *testing.T) { + ctx := context.Background() + store, err := OpenStore(filepath.Join(t.TempDir(), "qoder.db")) + if err != nil { + t.Fatal(err) + } + defer store.Close() + + base := time.Date(2026, 8, 28, 10, 15, 0, 0, time.UTC) + insert := func(at time.Time, status, model, account, kind string, latency, prompt, completion int, stream bool) { + t.Helper() + log := RequestLog{ + ID: NewRequestID(), CreatedAt: at, Status: status, RequestedModel: model, AccountID: account, + ErrorKind: kind, Stream: stream, AttemptCount: 1, + } + if latency > 0 { + log.LatencyMs = &latency + } + if prompt > 0 { + log.PromptTokens = &prompt + } + if completion > 0 { + log.CompletionTokens = &completion + } + if err := store.InsertRequestLog(ctx, log); err != nil { + t.Fatal(err) + } + } + insert(base, RequestStatusOK, "glm-5.3", "acc_a", "", 100, 12, 34, false) + insert(base.Add(20*time.Minute), RequestStatusOK, "glm-5.3", "acc_a", "", 200, 10, 20, true) + insert(base.Add(90*time.Minute), RequestStatusError, "qwen3.7-plus", "acc_b", KindRateLimit, 400, 8, 0, false) + insert(base.Add(2*time.Hour), RequestStatusCanceled, "glm-5.3", "acc_a", "", 0, 0, 0, false) + insert(base.Add(-30*time.Hour), RequestStatusOK, "old", "acc_a", "", 50, 1, 1, false) + + from := time.Date(2026, 8, 28, 10, 0, 0, 0, time.UTC) + to := time.Date(2026, 8, 28, 13, 0, 0, 0, time.UTC) + stats, err := store.SummarizeRequestLogs(ctx, from, to) + if err != nil { + t.Fatal(err) + } + if stats.Totals.Requests != 4 || stats.Totals.OK != 2 || stats.Totals.Error != 1 || stats.Totals.Canceled != 1 || stats.Totals.Streaming != 1 { + t.Fatalf("totals = %+v", stats.Totals) + } + if stats.Totals.SuccessRate != 0.5 { + t.Fatalf("success rate = %v", stats.Totals.SuccessRate) + } + if stats.Tokens.Prompt != 30 || stats.Tokens.Completion != 54 || stats.Tokens.Total != 84 { + t.Fatalf("tokens = %+v", stats.Tokens) + } + if stats.Latency.AvgMs == nil || *stats.Latency.AvgMs != 233 { + t.Fatalf("avg latency = %+v", stats.Latency.AvgMs) + } + if stats.Latency.P50Ms == nil || *stats.Latency.P50Ms != 200 { + t.Fatalf("p50 = %+v", stats.Latency.P50Ms) + } + if stats.Latency.P95Ms == nil || *stats.Latency.P95Ms != 400 { + t.Fatalf("p95 = %+v", stats.Latency.P95Ms) + } + if len(stats.Errors) != 1 || stats.Errors[0].Key != KindRateLimit || stats.Errors[0].Count != 1 { + t.Fatalf("errors = %+v", stats.Errors) + } + if len(stats.Models) == 0 || stats.Models[0].Key != "glm-5.3" || stats.Models[0].Count != 3 { + t.Fatalf("models = %+v", stats.Models) + } + if len(stats.Accounts) == 0 || stats.Accounts[0].Key != "acc_a" || stats.Accounts[0].Count != 3 { + t.Fatalf("accounts = %+v", stats.Accounts) + } + if len(stats.Series) != 3 { + t.Fatalf("series len = %d %+v", len(stats.Series), stats.Series) + } + if stats.Series[0].Requests != 2 || stats.Series[1].Requests != 1 || stats.Series[2].Requests != 1 { + t.Fatalf("series = %+v", stats.Series) + } + + empty, err := store.SummarizeRequestLogs(ctx, to.Add(time.Hour), to.Add(2*time.Hour)) + if err != nil { + t.Fatal(err) + } + if empty.Totals.Requests != 0 || empty.Latency.AvgMs != nil || len(empty.Models) != 0 { + t.Fatalf("empty stats = %+v", empty) + } + + hourFrom := time.Date(2026, 8, 28, 10, 0, 0, 0, time.UTC) + hourTo := hourFrom.Add(time.Hour) + hourStats, err := store.SummarizeRequestLogs(ctx, hourFrom, hourTo) + if err != nil { + t.Fatal(err) + } + if len(hourStats.Series) != 4 { + t.Fatalf("1h series len = %d %+v", len(hourStats.Series), hourStats.Series) + } + if hourStats.Series[1].Requests != 1 || hourStats.Series[2].Requests != 1 { + t.Fatalf("1h series = %+v", hourStats.Series) + } +} diff --git a/internal/api/logs.go b/internal/api/logs.go index 3ac5d9cf..5ec5a555 100644 --- a/internal/api/logs.go +++ b/internal/api/logs.go @@ -23,11 +23,46 @@ func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) { s.handleGetRequestLog(w, r, id) case path == "runtime" && r.Method == http.MethodGet: s.handleRuntimeLogs(w, r) + case path == "stats" && r.Method == http.MethodGet: + s.handleRequestStats(w, r) default: writeErr(w, http.StatusNotFound, "not_found", "unknown logs endpoint") } } +func (s *Server) handleRequestStats(w http.ResponseWriter, r *http.Request) { + if s.recorder == nil || s.recorder.Store() == nil { + writeErr(w, http.StatusServiceUnavailable, "logs_unavailable", "request logs unavailable") + return + } + now := time.Now().UTC() + hours := 24 + if raw := strings.TrimSpace(r.URL.Query().Get("hours")); raw != "" { + if n, err := strconv.Atoi(raw); err == nil { + hours = n + } + } + if hours != 1 && hours != 24 && hours != 168 { + hours = 24 + } + to := parseQueryTime(r.URL.Query().Get("to"), true) + if to == nil { + value := now + to = &value + } + from := parseQueryTime(r.URL.Query().Get("from"), false) + if from == nil { + value := to.Add(-time.Duration(hours) * time.Hour) + from = &value + } + stats, err := s.recorder.Store().SummarizeRequestLogs(r.Context(), *from, *to) + if err != nil { + writeErr(w, http.StatusInternalServerError, "stats_failed", err.Error()) + return + } + writeJSON(w, http.StatusOK, stats) +} + func (s *Server) handleListRequestLogs(w http.ResponseWriter, r *http.Request) { if s.recorder == nil || s.recorder.Store() == nil { writeErr(w, http.StatusServiceUnavailable, "logs_unavailable", "request logs unavailable") diff --git a/internal/api/logs_test.go b/internal/api/logs_test.go index c1bbf87d..9fe663ff 100644 --- a/internal/api/logs_test.go +++ b/internal/api/logs_test.go @@ -92,6 +92,54 @@ func TestListRequestLogsFiltersAndPagination(t *testing.T) { } } +func TestRequestStatsWindow(t *testing.T) { + dir := t.TempDir() + srv := New(config.Config{ + Host: "127.0.0.1", Port: 3010, ProxyAPIKey: "secret", + QoderHome: dir, DataDir: dir, + }) + defer srv.Close() + + ctx := context.Background() + base := time.Date(2026, 8, 28, 12, 10, 0, 0, time.UTC) + latency := 180 + prompt, completion := 11, 22 + if err := srv.recorder.Store().InsertRequestLog(ctx, accounts.RequestLog{ + ID: accounts.NewRequestID(), CreatedAt: base, Status: accounts.RequestStatusOK, + RequestedModel: "glm-5.3", AccountID: "acc_a", LatencyMs: &latency, + PromptTokens: &prompt, CompletionTokens: &completion, + }); err != nil { + t.Fatal(err) + } + if err := srv.recorder.Store().InsertRequestLog(ctx, accounts.RequestLog{ + ID: accounts.NewRequestID(), CreatedAt: base.Add(30 * time.Minute), Status: accounts.RequestStatusError, + RequestedModel: "qwen3.7-plus", AccountID: "acc_b", ErrorKind: accounts.KindUnavailable, + }); err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest(http.MethodGet, "/api/logs/stats?from=2026-08-28T12:00:00Z&to=2026-08-28T13:00:00Z", nil) + req.Header.Set("Authorization", "Bearer secret") + rec := httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + var stats accounts.RequestStats + if err := json.Unmarshal(rec.Body.Bytes(), &stats); err != nil { + t.Fatal(err) + } + if stats.Totals.Requests != 2 || stats.Totals.OK != 1 || stats.Totals.Error != 1 { + t.Fatalf("stats = %+v", stats.Totals) + } + if stats.Tokens.Total != 33 { + t.Fatalf("tokens = %+v", stats.Tokens) + } + if len(stats.Errors) != 1 || stats.Errors[0].Key != accounts.KindUnavailable { + t.Fatalf("errors = %+v", stats.Errors) + } +} + func TestParseQueryTimeDateOnlyEndOfDay(t *testing.T) { from := parseQueryTime("2026-08-28", false) to := parseQueryTime("2026-08-28", true) diff --git a/internal/logs/recorder.go b/internal/logs/recorder.go index e5cafa24..660b6476 100644 --- a/internal/logs/recorder.go +++ b/internal/logs/recorder.go @@ -16,6 +16,7 @@ type RequestStore interface { ClearRequestLogs(ctx context.Context) (int64, error) ListRequestLogs(ctx context.Context, filter accounts.RequestLogFilter) (accounts.RequestLogList, error) GetRequestLog(ctx context.Context, id string) (accounts.RequestLog, error) + SummarizeRequestLogs(ctx context.Context, from, to time.Time) (accounts.RequestStats, error) } type RequestRecorder struct { diff --git a/internal/webui/static/assets/index-xJt6fcS5.js b/internal/webui/static/assets/index-DzsGY5a_.js similarity index 63% rename from internal/webui/static/assets/index-xJt6fcS5.js rename to internal/webui/static/assets/index-DzsGY5a_.js index 0c24108d..b552e0e4 100644 --- a/internal/webui/static/assets/index-xJt6fcS5.js +++ b/internal/webui/static/assets/index-DzsGY5a_.js @@ -8,7 +8,7 @@ Error generating stack: `+e.message+` `+e.stack}}var be=Object.prototype.hasOwnProperty,xe=t.unstable_scheduleCallback,Se=t.unstable_cancelCallback,Ce=t.unstable_shouldYield,we=t.unstable_requestPaint,Te=t.unstable_now,Ee=t.unstable_getCurrentPriorityLevel,De=t.unstable_ImmediatePriority,Oe=t.unstable_UserBlockingPriority,ke=t.unstable_NormalPriority,Ae=t.unstable_LowPriority,je=t.unstable_IdlePriority,Me=t.log,Ne=t.unstable_setDisableYieldValue,Pe=null,Fe=null;function Ie(e){if(typeof Me==`function`&&Ne(e),Fe&&typeof Fe.setStrictMode==`function`)try{Fe.setStrictMode(Pe,e)}catch{}}var Le=Math.clz32?Math.clz32:Be,Re=Math.log,ze=Math.LN2;function Be(e){return e>>>=0,e===0?32:31-(Re(e)/ze|0)|0}var Ve=256,He=262144,Ue=4194304;function We(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Ge(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=We(n))):i=We(o):i=We(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=We(n))):i=We(o)):i=We(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Ke(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function qe(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Je(){var e=Ue;return Ue<<=1,!(Ue&62914560)&&(Ue=4194304),e}function Ye(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Xe(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Ze(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),un=!1;if(ln)try{var dn={};Object.defineProperty(dn,"passive",{get:function(){un=!0}}),window.addEventListener(`test`,dn,dn),window.removeEventListener(`test`,dn,dn)}catch{un=!1}var fn=null,pn=null,mn=null;function hn(){if(mn)return mn;var e,t=pn,n=t.length,r,i=`value`in fn?fn.value:fn.textContent,a=i.length;for(e=0;e=qn),Xn=` `,Zn=!1;function Qn(e,t){switch(e){case`keyup`:return Gn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function $n(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var er=!1;function tr(e,t){switch(e){case`compositionend`:return $n(t);case`keypress`:return t.which===32?(Zn=!0,Xn):null;case`textInput`:return e=t.data,e===Xn&&Zn?null:e;default:return null}}function nr(e,t){if(er)return e===`compositionend`||!Kn&&Qn(e,t)?(e=hn(),mn=pn=fn=null,er=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Cr(n)}}function Tr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Tr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Er(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=It(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=It(e.document)}return t}function Dr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Or=ln&&`documentMode`in document&&11>=document.documentMode,kr=null,Ar=null,jr=null,Mr=!1;function Nr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Mr||kr==null||kr!==It(r)||(r=kr,`selectionStart`in r&&Dr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),jr&&z(jr,r)||(jr=r,r=kd(Ar,`onSelect`),0>=o,i-=o,Ti=1<<32-Le(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),Pi&&Di(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),Pi&&Di(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return Pi&&Di(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),Pi&&Di(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&Oa(l)===r.type){n(e,r.sibling),c=a(r,o.props),Fa(c,o),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}o.type===y?(c=fi(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=di(o.type,o.key,o.props,null,e.mode,c),Fa(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l){if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}c=hi(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=Oa(o),b(e,r,o,c)}if(ee(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Pa(o),c);if(o.$$typeof===C)return b(e,r,na(e,o),c);Ia(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=pi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Na=0;var i=b(e,t,n,r);return Ma=null,i}catch(t){if(t===Sa||t===wa)throw t;var a=si(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ra=La(!0),za=La(!1),Ba=!1;function Va(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ha(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ua(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Wa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Ll&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=ii(e),ri(e,null,n),t}return ei(e,r,t,n),ii(e)}function Ga(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,$e(e,n)}}function Ka(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var qa=!1;function Ja(){if(qa){var e=pa;if(e!==null)throw e}}function Ya(e,t,n,r){qa=!1;var i=e.updateQueue;Ba=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Rl&f)===f:(r&f)===f){f!==0&&f===fa&&(qa=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:Ba=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Kl|=o,e.lanes=o,e.memoizedState=d}}function Xa(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Za(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=F.T,s={};F.T=s,Ns(e,!1,t,n);try{var c=i(),l=F.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ms(e,t,ga(c,r),mu(e)):Ms(e,t,r,mu(e))}catch(n){Ms(e,t,{then:function(){},status:`rejected`,reason:n},mu())}finally{I.p=a,o!==null&&s.types!==null&&(o.types=s.types),F.T=o}}function Ss(){}function Cs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=ws(e).queue;xs(e,a,t,L,n===null?Ss:function(){return Ts(e),n(r)})}function ws(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:L,baseState:L,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Fo,lastRenderedState:L},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Fo,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ts(e){var t=ws(e);t.next===null&&(t=e.alternate.memoizedState),Ms(e,t.next.queue,{},mu())}function Es(){return ta(tp)}function Ds(){return Ao().memoizedState}function Os(){return Ao().memoizedState}function ks(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=mu();e=Ua(n);var r=Wa(t,e,n);r!==null&&(gu(r,t,n),Ga(r,t,n)),t={cache:ca()},e.payload=t;return}t=t.return}}function As(e,t,n){var r=mu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Ps(e)?Fs(t,n):(n=ti(e,t,n,r),n!==null&&(gu(n,e,r),Is(n,t,r)))}function js(e,t,n){Ms(e,t,n,mu())}function Ms(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ps(e))Fs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Sr(s,o))return ei(e,t,i,0),J===null&&$r(),!1}catch{}if(n=ti(e,t,i,r),n!==null)return gu(n,e,r),Is(n,t,r),!0}return!1}function Ns(e,t,n,r){if(r={lane:2,revertLane:pd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ps(e)){if(t)throw Error(i(479))}else t=ti(e,n,r,2),t!==null&&gu(t,e,2)}function Ps(e){var t=e.alternate;return e===U||t!==null&&t===U}function Fs(e,t){mo=po=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Is(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,$e(e,n)}}var Ls={readContext:ta,use:No,useCallback:bo,useContext:bo,useEffect:bo,useImperativeHandle:bo,useLayoutEffect:bo,useInsertionEffect:bo,useMemo:bo,useReducer:bo,useRef:bo,useState:bo,useDebugValue:bo,useDeferredValue:bo,useTransition:bo,useSyncExternalStore:bo,useId:bo,useHostTransitionStatus:bo,useFormState:bo,useActionState:bo,useOptimistic:bo,useMemoCache:bo,useCacheRefresh:bo};Ls.useEffectEvent=bo;var Rs={readContext:ta,use:No,useCallback:function(e,t){return ko().memoizedState=[e,t===void 0?null:t],e},useContext:ta,useEffect:ls,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),ss(4194308,4,hs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ss(4194308,4,e,t)},useInsertionEffect:function(e,t){ss(4,2,e,t)},useMemo:function(e,t){var n=ko();t=t===void 0?null:t;var r=e();if(ho){Ie(!0);try{e()}finally{Ie(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=ko();if(n!==void 0){var i=n(t);if(ho){Ie(!0);try{n(t)}finally{Ie(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=As.bind(null,U,e),[r.memoizedState,e]},useRef:function(e){var t=ko();return e={current:e},t.memoizedState=e},useState:function(e){e=Go(e);var t=e.queue,n=js.bind(null,U,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:_s,useDeferredValue:function(e,t){return ys(ko(),e,t)},useTransition:function(){var e=Go(!1);return e=xs.bind(null,U,e.queue,!0,!1),ko().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=U,a=ko();if(Pi){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),J===null)throw Error(i(349));Rl&127||Bo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,ls(Ho.bind(null,r,o,e),[e]),r.flags|=2048,as(9,{destroy:void 0},Vo.bind(null,r,o,n,t),null),n},useId:function(){var e=ko(),t=J.identifierPrefix;if(Pi){var n=Ei,r=Ti;n=(r&~(1<<32-Le(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=go++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[ot]=t,o[st]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Rd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Mc(t)}}return Lc(t),Nc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Mc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=se.current,Vi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Mi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[ot]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Fd(e.nodeValue,n)),e||Ri(t,!0)}else e=Wd(e).createTextNode(r),e[ot]=t,t.stateNode=e}return Lc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Vi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[ot]=t}else Hi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Lc(t),e=!1}else n=Ui(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(oo(t),t):(oo(t),null);if(t.flags&128)throw Error(i(558))}return Lc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Vi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[ot]=t}else Hi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Lc(t),a=!1}else a=Ui(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(oo(t),t):(oo(t),null)}return oo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Fc(t,t.updateQueue),Lc(t),null);case 4:return ue(),e===null&&Td(t.stateNode.containerInfo),Lc(t),null;case 10:return Yi(t.type),Lc(t),null;case 19:if(R(so),r=t.memoizedState,r===null)return Lc(t),null;if(a=!!(t.flags&128),o=r.rendering,o===null){if(a)Ic(r,!1);else{if(Gl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=co(e),o!==null){for(t.flags|=128,Ic(r,!1),e=o.updateQueue,t.updateQueue=e,Fc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)ui(n,e),n=n.sibling;return ie(so,so.current&1|2),Pi&&Di(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Te()>nu&&(t.flags|=128,a=!0,Ic(r,!1),t.lanes=4194304)}}else{if(!a){if(e=co(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Fc(t,e),Ic(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!Pi)return Lc(t),null}else 2*Te()-r.renderingStartTime>nu&&n!==536870912&&(t.flags|=128,a=!0,Ic(r,!1),t.lanes=4194304)}r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Lc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Te(),e.sibling=null,n=so.current,ie(so,a?n&1|2:n&1),Pi&&Di(t,r.treeForkCount),e);case 22:case 23:return oo(t),B(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Lc(t),t.subtreeFlags&6&&(t.flags|=8192)):Lc(t),n=t.updateQueue,n!==null&&Fc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&R(va),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Yi(sa),Lc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function zc(e,t){switch(Ai(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Yi(sa),ue(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return fe(t),null;case 31:if(t.memoizedState!==null){if(oo(t),t.alternate===null)throw Error(i(340));Hi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(oo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Hi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return R(so),null;case 4:return ue(),null;case 10:return Yi(t.type),null;case 22:case 23:return oo(t),B(),e!==null&&R(va),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Yi(sa),null;case 25:return null;default:return null}}function Bc(e,t){switch(Ai(t),t.tag){case 3:Yi(sa),ue();break;case 26:case 27:case 5:fe(t);break;case 4:ue();break;case 31:t.memoizedState!==null&&oo(t);break;case 13:oo(t);break;case 19:R(so);break;case 10:Yi(t.type);break;case 22:case 23:oo(t),B(),e!==null&&R(va);break;case 24:Yi(sa)}}function Vc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Ku(t,t.return,e)}}function Hc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Ku(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Ku(t,t.return,e)}}function Uc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Za(t,n)}catch(t){Ku(e,e.return,t)}}}function Wc(e,t,n){n.props=Gs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Ku(e,t,n)}}function Gc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Ku(e,t,n)}}function Kc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null){if(typeof r==`function`)try{r()}catch(n){Ku(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Ku(e,t,n)}else n.current=null}}function qc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Ku(e,e.return,t)}}function Jc(e,t,n){try{var r=e.stateNode;zd(r,e.type,n,t),r[st]=t}catch(t){Ku(e,e.return,t)}}function Yc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&tf(e.type)||e.tag===4}function Xc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Yc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&tf(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Zc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=$t));else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Zc(e,t,n),e=e.sibling;e!==null;)Zc(e,t,n),e=e.sibling}function Qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Qc(e,t,n),e=e.sibling;e!==null;)Qc(e,t,n),e=e.sibling}function $c(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Rd(t,r,n),t[ot]=e,t[st]=n}catch(t){Ku(e,e.return,t)}}var el=!1,tl=!1,nl=!1,rl=typeof WeakSet==`function`?WeakSet:Set,il=null;function al(e,t){if(e=e.containerInfo,Hd=up,e=Er(e),Dr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(Ud={focusedElem:e,selectionRange:n},up=!1,il=t;il!==null;)if(t=il,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,il=e;else for(;il!==null;){switch(t=il,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Rd(o,r,n),o[ot]=e,yt(o),r=o;break a;case`link`:var s=Wf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=wr(s,h),v=wr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,F.T=null,n=uu,uu=null;var o=ou,s=cu;if(au=0,su=ou=null,cu=0,Ll&6)throw Error(i(331));var c=Ll;if(Ll|=4,Ml(o.current),Tl(o,o.current,s,n),Ll=c,od(0,!1),Fe&&typeof Fe.onPostCommitFiberRoot==`function`)try{Fe.onPostCommitFiberRoot(Pe,o)}catch{}return!0}finally{I.p=a,F.T=r,Hu(e,t)}}function Gu(e,t,n){t=_i(n,t),t=Zs(e.stateNode,t,2),e=Wa(e,t,2),e!==null&&(Xe(e,2),ad(e))}function Ku(e,t,n){if(e.tag===3)Gu(e,e,n);else for(;t!==null;){if(t.tag===3){Gu(t,e,n);break}if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(iu===null||!iu.has(r))){e=_i(n,e),n=Qs(2),r=Wa(t,n,2),r!==null&&($s(n,r,t,e),Xe(r,2),ad(r));break}}t=t.return}}function qu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Il;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ju.bind(null,e,t,n),t.then(e,e))}function Ju(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,J===e&&(Rl&n)===n&&(Gl===4||Gl===3&&(Rl&62914560)===Rl&&300>Te()-eu?!(Ll&2)&&Cu(e,0):Jl|=n,Xl===Rl&&(Xl=0)),ad(e)}function Yu(e,t){t===0&&(t=Je()),e=ni(e,t),e!==null&&(Xe(e,t),ad(e))}function Xu(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Yu(e,n)}function Zu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Yu(e,n)}function Qu(e,t){return xe(e,t)}var $u=null,ed=null,td=!1,nd=!1,rd=!1,id=0;function ad(e){e!==ed&&e.next===null&&(ed===null?$u=ed=e:ed=ed.next=e),nd=!0,td||(td=!0,fd())}function od(e,t){if(!rd&&nd){rd=!0;do for(var n=!1,r=$u;r!==null;){if(!t){if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Le(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,dd(r,a))}else a=Rl,a=Ge(r,r===J?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ke(r,a)||(n=!0,dd(r,a))}r=r.next}while(n);rd=!1}}function sd(){cd()}function cd(){nd=td=!1;var e=0;id!==0&&Yd()&&(e=id);for(var t=Te(),n=null,r=$u;r!==null;){var i=r.next,a=ld(r,t);a===0?(r.next=null,n===null?$u=i:n.next=i,i===null&&(ed=n)):(n=r,(e!==0||a&3)&&(nd=!0)),r=i}au!==0&&au!==5||od(e,!1),id!==0&&(id=0)}function ld(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Bd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function Tf(e,t,n){var r=wf;if(r&&typeof t==`string`&&t){var i=Rt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),yf.has(i)||(yf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Rd(t,`link`,e),yt(t),r.head.appendChild(t)))}}function Ef(e){xf.D(e),Tf(`dns-prefetch`,e,null)}function Df(e,t){xf.C(e,t),Tf(`preconnect`,e,t)}function Of(e,t,n){xf.L(e,t,n);var r=wf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Rt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Rt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Rt(n.imageSizes)+`"]`)):i+=`[href="`+Rt(e)+`"]`;var a=i;switch(t){case`style`:a=Pf(e);break;case`script`:a=Rf(e)}vf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),vf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Ff(a))||t===`script`&&r.querySelector(zf(a))||(t=r.createElement(`link`),Rd(t,`link`,e),yt(t),r.head.appendChild(t)))}}function kf(e,t){xf.m(e,t);var n=wf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Rt(r)+`"][href="`+Rt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Rf(e)}if(!vf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),vf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(zf(a)))return}r=n.createElement(`link`),Rd(r,`link`,e),yt(r),n.head.appendChild(r)}}}function Af(e,t,n){xf.S(e,t,n);var r=wf;if(r&&e){var i=vt(r).hoistableStyles,a=Pf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Ff(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=vf.get(a))&&Hf(e,n);var c=o=r.createElement(`link`);yt(c),Rd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Vf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function jf(e,t){xf.X(e,t);var n=wf;if(n&&e){var r=vt(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=h({src:e,async:!0},t),(t=vf.get(i))&&X(e,t),a=n.createElement(`script`),yt(a),Rd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Mf(e,t){xf.M(e,t);var n=wf;if(n&&e){var r=vt(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=vf.get(i))&&X(e,t),a=n.createElement(`script`),yt(a),Rd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Nf(e,t,n,r){var a=(a=se.current)?bf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Pf(n.href),n=vt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Pf(n.href);var o=vt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(Ff(e)))&&!o._p&&(s.instance=o,s.state.loading=5),vf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},vf.set(e,n),o||Lf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Rf(n),n=vt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Pf(e){return`href="`+Rt(e)+`"`}function Ff(e){return`link[rel="stylesheet"][`+e+`]`}function If(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Lf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Rd(t,`link`,n),yt(t),e.head.appendChild(t))}function Rf(e){return`[src="`+Rt(e)+`"]`}function zf(e){return`script[async]`+e}function Bf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Rt(n.href)+`"]`);if(r)return t.instance=r,yt(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),yt(r),Rd(r,`style`,a),Vf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Pf(n.href);var o=e.querySelector(Ff(a));if(o)return t.state.loading|=4,t.instance=o,yt(o),o;r=If(n),(a=vf.get(a))&&Hf(r,a),o=(e.ownerDocument||e).createElement(`link`),yt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Rd(o,`link`,r),t.state.loading|=4,Vf(o,n.precedence,e),t.instance=o;case`script`:return o=Rf(n.src),(a=e.querySelector(zf(o)))?(t.instance=a,yt(a),a):(r=n,(a=vf.get(o))&&(r=h({},n),X(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),yt(a),Rd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Vf(r,n.precedence,e));return t.instance}function Vf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Kf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function qf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Jf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Pf(r.href),a=t.querySelector(Ff(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Zf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,yt(a);return}a=t.ownerDocument||t,r=If(r),(i=vf.get(i))&&Hf(r,i),a=a.createElement(`link`),yt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Rd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Zf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Yf=0;function Xf(e,t){return e.stylesheets&&e.count===0&&$f(e,e.stylesheets),0Yf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Zf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)$f(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Qf=null;function $f(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Qf=new Map,t.forEach(ep,e),Qf=null,Zf.call(e))}function ep(e,t){if(!(t.state.loading&4)){var n=Qf.get(e);if(n)var r=n.get(null);else{n=new Map,Qf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()})),_=c(u(),1),v=g(),y=`modulepreload`,b=function(e){return`/`+e},x={},S=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=b(t,n),t=s(t),t in x)return;x[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:y,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},C=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,w=/^[\\/]{2}/;function T(e,t){return t+e.replace(/\\/g,`/`)}var E=`popstate`;function D(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function O(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return N(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:P(t)}return F(t,n,null,e)}function k(e,t){if(e===!1||e==null)throw Error(t)}function A(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function j(){return Math.random().toString(36).substring(2,10)}function M(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function N(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?ee(t):t,state:n,key:t&&t.key||r||j(),mask:i}}function P({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function ee(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function F(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=D(e)?e:N(h.location,e,t);n&&n(r,e),l=u()+1;let d=M(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=D(e)?e:N(h.location,e,t);n&&n(r,e),l=u();let i=M(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return I(i,e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(E,d),c=e,()=>{i.removeEventListener(E,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function I(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),k(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:P(t);return i=i.replace(/ $/,`%20`),!n&&w.test(i)&&(i=r+i),new URL(i,r)}function L(e,t,n=`/`){return te(e,t,n,!1)}function te(e,t,n,r,i){let a=ye((typeof t==`string`?ee(t):t).pathname||`/`,n);if(a==null)return null;let o=i??ne(e),s=null,c=ve(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;k(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=De([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(k(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),re(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:fe(l,e.index),routesMeta:u.map((e,t)=>{let[n,r]=_e(e.relativePath,e.caseSensitive,t===u.length-1);return{...e,matcher:n,compiledParams:r}})})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of R(e.path))a(e,t,!0,n)}),t}function R(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=R(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function ie(e){e.sort((e,t)=>e.score===t.score?pe(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var ae=/^:[\w-]+$/,oe=3,se=2,ce=1,le=10,ue=-2,de=e=>e===`*`;function fe(e,t){let n=e.split(`/`),r=n.length;return n.some(de)&&(r+=ue),t&&(r+=se),n.filter(e=>!de(e)).reduce((e,t)=>e+(ae.test(t)?oe:t===``?ce:le),r)}function pe(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function me(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return e[t]=n&&!i?void 0:(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function _e(e,t=!1,n=!0){A(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function ve(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return A(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function ye(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}function be(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?ee(e):e,a;return n?(n=Ee(n),a=n.startsWith(`/`)?xe(n.substring(1),`/`):xe(n,t)):a=t,{pathname:a,search:Ae(r),hash:je(i)}}function xe(e,t){let n=Oe(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function Se(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Ce(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function we(e){let t=Ce(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function Te(e,t,n,r=!1){let i;typeof e==`string`?i=ee(e):(i={...e},k(!i.pathname||!i.pathname.includes(`?`),Se(`?`,`pathname`,`search`,i)),k(!i.pathname||!i.pathname.includes(`#`),Se(`#`,`pathname`,`hash`,i)),k(!i.search||!i.search.includes(`#`),Se(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=be(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Ee=e=>e.replace(/[\\/]{2,}/g,`/`),De=e=>Ee(e.join(`/`)),Oe=e=>e.replace(/\/+$/,``),ke=e=>Oe(e).replace(/^\/*/,`/`),Ae=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,je=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Me=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Ne(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Pe(e){return De(e.map(e=>e.route.path).filter(Boolean))||`/`}var Fe=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Ie(e,t){let n=e;if(typeof n!=`string`||!C.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Fe)try{let e=new URL(window.location.href),r=w.test(n)?new URL(T(n,e.protocol)):new URL(n),a=ye(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{A(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Le=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Le);var Re=[`GET`,...Le];new Set(Re);var ze=[`about:`,`blob:`,`chrome:`,`chrome-untrusted:`,`content:`,`data:`,`devtools:`,`file:`,`filesystem:`,`javascript:`];function Be(e){try{return ze.includes(new URL(e).protocol)}catch{return!1}}var Ve=_.createContext(null);Ve.displayName=`DataRouter`;var He=_.createContext(null);He.displayName=`DataRouterState`;var Ue=_.createContext(!1);function We(){return _.useContext(Ue)}var Ge=_.createContext({isTransitioning:!1});Ge.displayName=`ViewTransition`;var Ke=_.createContext(new Map);Ke.displayName=`Fetchers`;var qe=_.createContext(null);qe.displayName=`Await`;var Je=_.createContext(null);Je.displayName=`Navigation`;var Ye=_.createContext(null);Ye.displayName=`Location`;var Xe=_.createContext({outlet:null,matches:[],isDataRoute:!1});Xe.displayName=`Route`;var Ze=_.createContext(null);Ze.displayName=`RouteError`;var Qe=`REACT_ROUTER_ERROR`,$e=`REDIRECT`,et=`ROUTE_ERROR_RESPONSE`;function tt(e){if(e.startsWith(`${Qe}:${$e}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function nt(e){if(e.startsWith(`${Qe}:${et}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Me(t.status,t.statusText,t.data)}catch{}}function rt(e,{relative:t}={}){k(it(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=_.useContext(Je),{hash:i,pathname:a,search:o}=ft(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:De([n,a])),r.createHref({pathname:s,search:o,hash:i})}function it(){return _.useContext(Ye)!=null}function at(){return k(it(),`useLocation() may be used only in the context of a component.`),_.useContext(Ye).location}var ot=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function st(e){_.useContext(Je).static||_.useLayoutEffect(e)}function ct(){let{isDataRoute:e}=_.useContext(Xe);return e?kt():lt()}function lt(){k(it(),`useNavigate() may be used only in the context of a component.`);let e=_.useContext(Ve),{basename:t,navigator:n}=_.useContext(Je),{matches:r}=_.useContext(Xe),{pathname:i}=at(),a=JSON.stringify(we(r)),o=_.useRef(!1);return st(()=>{o.current=!0}),_.useCallback((r,s={})=>{if(A(o.current,ot),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=Te(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:De([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var ut=_.createContext(null);function dt(e){let t=_.useContext(Xe).outlet;return _.useMemo(()=>t&&_.createElement(ut.Provider,{value:e},t),[t,e])}function ft(e,{relative:t}={}){let{matches:n}=_.useContext(Xe),{pathname:r}=at(),i=JSON.stringify(we(n));return _.useMemo(()=>Te(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function pt(e,t){return mt(e,t)}function mt(e,t,n){k(it(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=_.useContext(Je),{matches:i}=_.useContext(Xe),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;jt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. -Please change the parent to .`)}let u=at(),d;if(t){let e=typeof t==`string`?ee(t):t;k(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):L(e,{pathname:p});A(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),A(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=xt(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:De([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:De([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?_.createElement(Ye.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function ht(){let e=Ot(),t=Ne(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=_.createElement(_.Fragment,null,_.createElement(`p`,null,`💿 Hey developer 👋`),_.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,_.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,_.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),_.createElement(_.Fragment,null,_.createElement(`h2`,null,`Unexpected Application Error!`),_.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?_.createElement(`pre`,{style:i},n):null,o)}var gt=_.createElement(ht,null),_t=class extends _.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=nt(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:_.createElement(Xe.Provider,{value:this.props.routeContext},_.createElement(Ze.Provider,{value:e,children:this.props.component}));return this.context?_.createElement(yt,{error:e},t):t}};_t.contextType=Ue;var vt=new WeakMap;function yt({children:e,error:t}){let{basename:n}=_.useContext(Je);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=tt(t.digest);if(e){let r=vt.get(t);if(r)throw r;let i=Ie(e.location,n),a=i.absoluteURL||i.to;if(Be(a))throw Error(`Invalid redirect location`);if(Fe&&!vt.get(t)){if(i.isExternal||e.reloadDocument)window.location.href=a;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw vt.set(t,n),n}}return _.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${a}`})}}return e}function bt({routeContext:e,match:t,children:n}){let r=_.useContext(Ve);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),_.createElement(Xe.Provider,{value:e},n)}function xt(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);k(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Pe(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||gt,o&&(s<0&&c===0?(jt(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?_.createElement(n.route.Component,null):n.route.element?n.route.element:e,_.createElement(bt,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?_.createElement(_t,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function St(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Ct(e){let t=_.useContext(Ve);return k(t,St(e)),t}function wt(e){let t=_.useContext(He);return k(t,St(e)),t}function Tt(e){let t=_.useContext(Xe);return k(t,St(e)),t}function Et(e){let t=Tt(e),n=t.matches[t.matches.length-1];return k(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function Dt(){return Et(`useRouteId`)}function Ot(){let e=_.useContext(Ze),t=wt(`useRouteError`),n=Et(`useRouteError`);return e===void 0?t.errors?.[n]:e}function kt(){let{router:e}=Ct(`useNavigate`),t=Et(`useNavigate`),n=_.useRef(!1);return st(()=>{n.current=!0}),_.useCallback(async(r,i={})=>{A(n.current,ot),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var At={};function jt(e,t,n){!t&&!At[e]&&(At[e]=!0,A(!1,n))}_.memo(Mt);function Mt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return mt(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Nt({to:e,replace:t,state:n,relative:r}){k(it(),` may be used only in the context of a component.`);let{static:i}=_.useContext(Je);A(!i,` must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.`);let{matches:a}=_.useContext(Xe),{pathname:o}=at(),s=ct(),c=Te(e,we(a),o,r===`path`),l=JSON.stringify(c);return _.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function Pt(e){return dt(e.context)}function Ft(e){k(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function It({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){k(!it(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=_.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=ee(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=_.useMemo(()=>{let e=ye(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return A(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:_.createElement(Je.Provider,{value:c},_.createElement(Ye.Provider,{children:t,value:h}))}function Lt({children:e,location:t}){return pt(Rt(e),t)}_.Component;function Rt(e,t=[]){let n=[];return _.Children.forEach(e,(e,r)=>{if(!_.isValidElement(e))return;let i=[...t,r];if(e.type===_.Fragment){n.push.apply(n,Rt(e.props.children,i));return}k(e.type===Ft,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),k(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=Rt(e.props.children,i)),n.push(a)}),n}var zt=`get`,Bt=`application/x-www-form-urlencoded`;function Vt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Ht(e){return Vt(e)&&e.tagName.toLowerCase()===`button`}function Ut(e){return Vt(e)&&e.tagName.toLowerCase()===`form`}function Wt(e){return Vt(e)&&e.tagName.toLowerCase()===`input`}function Gt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Kt(e,t){return e.button===0&&(!t||t===`_self`)&&!Gt(e)}var qt=null;function Jt(){if(qt===null)try{new FormData(document.createElement(`form`),0),qt=!1}catch{qt=!0}return qt}var Yt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Xt(e){return e!=null&&!Yt.has(e)?(A(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Bt}"`),null):e}function Zt(e,t){let n,r,i,a,o;if(Ut(e)){let o=e.getAttribute(`action`);r=o?ye(o,t):null,n=e.getAttribute(`method`)||zt,i=Xt(e.getAttribute(`enctype`))||Bt,a=new FormData(e)}else if(Ht(e)||Wt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a