From 0e6528b3668a9a43cf76049dfb8d1b65c99077c1 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 27 May 2026 13:04:25 +0000 Subject: [PATCH] Add live Hermes operations dashboard --- .gitignore | 2 + .../backend/src/modules/env/repository.ts | 31 ++ dashboard/backend/src/modules/env/routes.ts | 48 ++ dashboard/backend/src/modules/env/types.ts | 22 + .../src/modules/hermes-ops/repository.ts | 215 +++++++++ .../backend/src/modules/hermes-ops/routes.ts | 13 + .../backend/src/modules/hermes-ops/types.ts | 51 ++ dashboard/backend/src/server.ts | 2 + dashboard/pnpm-lock.yaml | 441 +++++++++++++++++- dashboard/web/src/app/globals.css | 5 + dashboard/web/src/app/hermes/agents/page.tsx | 2 +- dashboard/web/src/app/hermes/page.tsx | 19 +- .../web/src/app/hermes/products/page.tsx | 2 +- .../web/src/app/hermes/tasks/[id]/page.tsx | 4 +- dashboard/web/src/app/hermes/tasks/page.tsx | 4 +- dashboard/web/src/app/layout.tsx | 5 +- .../web/src/components/hermes-ops-panel.tsx | 216 +++++++++ .../web/src/components/ui/Primitives.tsx | 5 +- dashboard/web/src/lib/api.ts | 53 +++ 19 files changed, 1119 insertions(+), 21 deletions(-) create mode 100644 dashboard/backend/src/modules/env/repository.ts create mode 100644 dashboard/backend/src/modules/env/routes.ts create mode 100644 dashboard/backend/src/modules/env/types.ts create mode 100644 dashboard/backend/src/modules/hermes-ops/repository.ts create mode 100644 dashboard/backend/src/modules/hermes-ops/routes.ts create mode 100644 dashboard/backend/src/modules/hermes-ops/types.ts create mode 100644 dashboard/web/src/components/hermes-ops-panel.tsx diff --git a/.gitignore b/.gitignore index f876b09..76920a0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ __pycache__/ venv/ env/ ENV/ +!dashboard/backend/src/modules/env/ +!dashboard/backend/src/modules/env/** # IDE files .vscode/ diff --git a/dashboard/backend/src/modules/env/repository.ts b/dashboard/backend/src/modules/env/repository.ts new file mode 100644 index 0000000..d286638 --- /dev/null +++ b/dashboard/backend/src/modules/env/repository.ts @@ -0,0 +1,31 @@ +import type { EnvVar } from './types.js'; + +const envVars = new Map(); + +export async function getEnvVars(): Promise { + return Array.from(envVars.values()).sort((a, b) => a.name.localeCompare(b.name)); +} + +export async function getEnvVar(id: string): Promise { + return envVars.get(id) ?? null; +} + +export async function upsertEnvVar(input: Partial & { name: string }): Promise { + const id = input.id || input.name.toLowerCase().replace(/[^a-z0-9_]+/g, '_'); + const envVar: EnvVar = { + id, + name: input.name, + value: input.isSecret ? 'REDACTED' : input.value ?? '', + isSecret: input.isSecret ?? true, + source: input.source ?? 'local', + azureKeyVaultName: input.azureKeyVaultName, + azureSecretName: input.azureSecretName, + updatedAt: new Date().toISOString(), + }; + envVars.set(id, envVar); + return envVar; +} + +export async function deleteEnvVar(id: string): Promise { + return envVars.delete(id); +} diff --git a/dashboard/backend/src/modules/env/routes.ts b/dashboard/backend/src/modules/env/routes.ts new file mode 100644 index 0000000..1327e61 --- /dev/null +++ b/dashboard/backend/src/modules/env/routes.ts @@ -0,0 +1,48 @@ +import type { FastifyInstance } from 'fastify'; +import { BadRequestError } from '../../lib/auth.js'; +import { deleteEnvVar, getEnvVar, getEnvVars, upsertEnvVar } from './repository.js'; +import { EnvVarInputSchema, EnvVarParamsSchema } from './types.js'; + +export async function envRoutes(fastify: FastifyInstance) { + fastify.get('/env', async (req, reply) => { + return reply.send(await getEnvVars()); + }); + + fastify.get('/env/:id', async (req, reply) => { + const params = EnvVarParamsSchema.parse(req.params); + const envVar = await getEnvVar(params.id); + if (!envVar) return reply.code(404).send({ error: 'Environment variable not found' }); + return reply.send(envVar); + }); + + fastify.post('/env', async (req, reply) => { + try { + const input = EnvVarInputSchema.parse(req.body) as { name: string }; + return reply.code(201).send(await upsertEnvVar(input)); + } catch (error) { + if (error instanceof Error) throw new BadRequestError(error.message); + throw error; + } + }); + + fastify.put('/env/:id', async (req, reply) => { + try { + const params = EnvVarParamsSchema.parse(req.params); + const input = EnvVarInputSchema.parse({ ...(req.body as object), id: params.id }) as { name: string; id: string }; + return reply.send(await upsertEnvVar(input)); + } catch (error) { + if (error instanceof Error) throw new BadRequestError(error.message); + throw error; + } + }); + + fastify.delete('/env/:id', async (req, reply) => { + const params = EnvVarParamsSchema.parse(req.params); + await deleteEnvVar(params.id); + return reply.code(204).send(); + }); + + fastify.post('/env/sync-azure', async (req, reply) => { + return reply.send({ synced: 0, errors: ['Azure Key Vault sync is not configured in this local dashboard build.'] }); + }); +} diff --git a/dashboard/backend/src/modules/env/types.ts b/dashboard/backend/src/modules/env/types.ts new file mode 100644 index 0000000..64c3903 --- /dev/null +++ b/dashboard/backend/src/modules/env/types.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +export const EnvVarSchema = z.object({ + id: z.string().min(1), + name: z.string().min(1), + value: z.string().default(''), + isSecret: z.boolean().default(true), + source: z.enum(['local', 'azure-key-vault']).default('local'), + azureKeyVaultName: z.string().optional(), + azureSecretName: z.string().optional(), + updatedAt: z.string().datetime().default(() => new Date().toISOString()), +}); + +export const EnvVarParamsSchema = z.object({ + id: z.string().min(1), +}); + +export const EnvVarInputSchema = EnvVarSchema.omit({ name: true }).partial().extend({ + name: z.string().min(1), +}); + +export type EnvVar = z.infer; diff --git a/dashboard/backend/src/modules/hermes-ops/repository.ts b/dashboard/backend/src/modules/hermes-ops/repository.ts new file mode 100644 index 0000000..fe252aa --- /dev/null +++ b/dashboard/backend/src/modules/hermes-ops/repository.ts @@ -0,0 +1,215 @@ +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { readFile, stat } from 'fs/promises'; +import { existsSync } from 'fs'; +import type { HermesOpsInstance, HermesOpsRepo, HermesOpsSnapshot, HermesOpsTimer } from './types.js'; + +const execFileAsync = promisify(execFile); + +const instances = [ + { + id: 'vijay' as const, + label: 'Vijay / root', + hermesHome: '/root/.hermes', + gatewayKind: 'system', + gatewayService: 'hermes-gateway.service', + dashboardService: 'hermes-root-dashboard.service', + dashboardPort: 9119, + backupTimer: 'hermes-root-backup.timer', + repoPath: '/root/repos/bytelyst_hostinger_hermes_vm', + driveFolder: 'Vijay Drive', + }, + { + id: 'bheem' as const, + label: 'Bheem / Uma', + hermesHome: '/home/uma/.hermes', + gatewayKind: 'uma-user', + gatewayService: 'uma-hermes-gateway.service', + dashboardService: 'uma-hermes-dashboard.service', + dashboardPort: 9120, + backupTimer: 'uma-hermes-backup.timer', + repoPath: '/home/uma/repos/uma_hostinger_hermes_vm', + driveFolder: 'Bheem Drive', + }, +]; + +async function run(command: string, args: string[], cwd?: string): Promise { + try { + const { stdout } = await execFileAsync(command, args, { + cwd, + timeout: 5000, + maxBuffer: 1024 * 1024, + }); + return stdout.trim(); + } catch { + return null; + } +} + +async function isActive(unit: string): Promise { + return (await run('systemctl', ['is-active', unit])) === 'active'; +} + +async function isEnabled(unit: string): Promise { + return (await run('systemctl', ['is-enabled', unit])) === 'enabled'; +} + +async function getTimer(name: string): Promise { + const active = await isActive(name); + const show = await run('systemctl', [ + 'show', + name, + '-p', + 'NextElapseUSecRealtime', + '-p', + 'LastTriggerUSec', + '--no-pager', + ]); + const properties = Object.fromEntries( + (show ?? '') + .split('\n') + .map((line) => { + const [key, ...value] = line.split('='); + return [key, value.join('=') || null] as const; + }) + .filter(([key]) => key), + ); + + return { + name, + active, + nextRun: properties.NextElapseUSecRealtime ?? null, + lastRun: properties.LastTriggerUSec ?? null, + }; +} + +async function isUmaGatewayActive(): Promise { + const output = await run('ps', ['-eo', 'user=,args=']); + return Boolean( + output?.split('\n').some((line) => { + const trimmed = line.trimStart(); + return trimmed.startsWith('uma ') && trimmed.includes('hermes_cli.main gateway'); + }), + ); +} + +async function isUmaGatewayEnabled(): Promise { + return existsSync('/home/uma/.config/systemd/user/default.target.wants/uma-hermes-gateway.service'); +} + +async function getRepo(path: string): Promise { + const [branch, status, head, lastCommitAt, size] = await Promise.all([ + run('git', ['branch', '--show-current'], path), + run('git', ['status', '--porcelain'], path), + run('git', ['rev-parse', '--short', 'HEAD'], path), + run('git', ['log', '-1', '--format=%cI'], path), + run('du', ['-sh', '.git', 'hermes_persistent_backup'], path), + ]); + + return { + path, + branch: branch || null, + clean: status === '', + head: head || null, + lastCommitAt: lastCommitAt || null, + size: size ? size.replace(/\n/g, ' / ') : null, + }; +} + +async function manifestStats(hermesHome: string): Promise<{ files: number | null; cronJobs: number | null }> { + try { + const manifestPath = `${hermesHome}/MANIFEST.json`; + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as { files?: unknown[] }; + const jobsPath = `${hermesHome}/cron/jobs.json`; + const jobs = JSON.parse(await readFile(jobsPath, 'utf8')); + const cronJobs = Array.isArray(jobs) ? jobs.length : Array.isArray(jobs?.jobs) ? jobs.jobs.length : null; + return { + files: Array.isArray(manifest.files) ? manifest.files.length : null, + cronJobs, + }; + } catch { + return { files: null, cronJobs: null }; + } +} + +async function tokenExists(path: string): Promise { + try { + const info = await stat(path); + return info.isFile() && info.size > 100; + } catch { + return false; + } +} + +async function getTailscaleIp(): Promise { + const output = await run('tailscale', ['ip', '-4']); + return output?.split('\n')[0] || null; +} + +export async function getHermesOpsSnapshot(): Promise { + const tailscaleIp = await getTailscaleIp(); + const warnings: string[] = []; + const emergencyDriveUpload = await getTimer('hermes-emergency-drive-upload.timer'); + + const results: HermesOpsInstance[] = []; + for (const item of instances) { + const gatewayActiveCheck = + item.gatewayKind === 'uma-user' ? isUmaGatewayActive() : isActive(item.gatewayService); + const gatewayEnabledCheck = + item.gatewayKind === 'uma-user' ? isUmaGatewayEnabled() : isEnabled(item.gatewayService); + const [gatewayActive, gatewayEnabled, dashboardActive, backupTimer, repo, stats, googleToken] = await Promise.all([ + gatewayActiveCheck, + gatewayEnabledCheck, + isActive(item.dashboardService), + getTimer(item.backupTimer), + getRepo(item.repoPath), + manifestStats(`${item.repoPath}/hermes_persistent_backup`), + tokenExists(`${item.hermesHome}/google_token.json`), + ]); + + const dashboardUrl = tailscaleIp ? `http://${tailscaleIp}:${item.dashboardPort}/` : `:${item.dashboardPort}`; + if (!gatewayActive) warnings.push(`${item.label} gateway is not active`); + if (!backupTimer.active) warnings.push(`${item.label} backup timer is not active`); + if (!repo.clean) warnings.push(`${item.label} backup repo has uncommitted changes`); + if (!googleToken) warnings.push(`${item.label} Google Workspace token is missing`); + + results.push({ + id: item.id, + label: item.label, + hermesHome: item.hermesHome, + gateway: { + service: item.gatewayService, + active: gatewayActive, + enabled: gatewayEnabled, + }, + dashboard: { + service: item.dashboardService, + active: dashboardActive, + url: dashboardUrl, + }, + backup: { + timer: backupTimer, + repo, + restoredFileCount: stats.files, + restoredCronJobs: stats.cronJobs, + }, + google: { + workspaceToken: googleToken, + driveFolder: item.driveFolder, + }, + }); + } + + if (!emergencyDriveUpload.active) warnings.push('Emergency Google Drive upload timer is not active'); + if (!existsSync('/root/.config/hermes-google-drive/user-token.json')) { + warnings.push('Emergency Drive OAuth token is missing'); + } + + return { + generatedAt: new Date().toISOString(), + tailscaleIp, + emergencyDriveUpload, + instances: results, + warnings, + }; +} diff --git a/dashboard/backend/src/modules/hermes-ops/routes.ts b/dashboard/backend/src/modules/hermes-ops/routes.ts new file mode 100644 index 0000000..356b5e5 --- /dev/null +++ b/dashboard/backend/src/modules/hermes-ops/routes.ts @@ -0,0 +1,13 @@ +import type { FastifyInstance } from 'fastify'; +import { getHermesOpsSnapshot } from './repository.js'; + +export async function hermesOpsRoutes(fastify: FastifyInstance) { + fastify.get('/hermes/ops', async (req, reply) => { + try { + return reply.send(await getHermesOpsSnapshot()); + } catch (error) { + fastify.log.error(error, 'Failed to get Hermes operations snapshot'); + return reply.code(500).send({ error: 'Failed to get Hermes operations snapshot' }); + } + }); +} diff --git a/dashboard/backend/src/modules/hermes-ops/types.ts b/dashboard/backend/src/modules/hermes-ops/types.ts new file mode 100644 index 0000000..1a5cb37 --- /dev/null +++ b/dashboard/backend/src/modules/hermes-ops/types.ts @@ -0,0 +1,51 @@ +export interface HermesOpsTimer { + name: string; + active: boolean; + nextRun: string | null; + lastRun: string | null; +} + +export interface HermesOpsRepo { + path: string; + branch: string | null; + clean: boolean; + head: string | null; + lastCommitAt: string | null; + size: string | null; +} + +export interface HermesOpsGoogle { + workspaceToken: boolean; + driveFolder: string; +} + +export interface HermesOpsInstance { + id: 'vijay' | 'bheem'; + label: string; + hermesHome: string; + gateway: { + service: string; + active: boolean; + enabled: boolean; + }; + dashboard: { + service: string; + active: boolean; + url: string; + }; + backup: { + timer: HermesOpsTimer; + repo: HermesOpsRepo; + restoredFileCount: number | null; + restoredCronJobs: number | null; + }; + google: HermesOpsGoogle; +} + +export interface HermesOpsSnapshot { + generatedAt: string; + tailscaleIp: string | null; + emergencyDriveUpload: HermesOpsTimer; + instances: HermesOpsInstance[]; + warnings: string[]; +} diff --git a/dashboard/backend/src/server.ts b/dashboard/backend/src/server.ts index d4112c9..bb14354 100644 --- a/dashboard/backend/src/server.ts +++ b/dashboard/backend/src/server.ts @@ -13,6 +13,7 @@ import { envRoutes } from './modules/env/routes.js'; import { azureConfigRoutes } from './modules/azure-config/routes.js'; import { codeQualityRoutes } from './modules/code-quality/routes.js'; import { cosmosConfigRoutes } from './modules/cosmos-config/routes.js'; +import { hermesOpsRoutes } from './modules/hermes-ops/routes.js'; // import sse from 'fastify-sse-v2'; import rateLimit from '@fastify/rate-limit'; import swagger from '@fastify/swagger'; @@ -269,6 +270,7 @@ await fastify.register(envRoutes, { prefix: '/api' }); await fastify.register(azureConfigRoutes, { prefix: '/api' }); await fastify.register(codeQualityRoutes, { prefix: '/api' }); await fastify.register(cosmosConfigRoutes, { prefix: '/api' }); +await fastify.register(hermesOpsRoutes, { prefix: '/api' }); // Start server async function start() { diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml index b1e9941..7690269 100644 --- a/dashboard/pnpm-lock.yaml +++ b/dashboard/pnpm-lock.yaml @@ -4,7 +4,7 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -pnpmfileChecksum: sha256-49J7NwJd6tSrg/tZNqno9ed7ZkzTtWrmuMImMVilTD4= +pnpmfileChecksum: sha256-PZ6HEFLRffqcKTbFrfit0sMEROnXdhFHo8lSAewcLhA= importers: @@ -49,6 +49,9 @@ importers: '@types/node': specifier: ^25.0.3 version: 25.6.2 + '@vitest/coverage-v8': + specifier: 3.2.4 + version: 3.2.4(vitest@3.2.4(@types/node@25.6.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -104,6 +107,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.7.0(vite@7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) + '@vitest/coverage-v8': + specifier: 3.2.4 + version: 3.2.4(vitest@3.2.4(@types/node@25.6.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) autoprefixer: specifier: ^10.4.20 version: 10.5.0(postcss@8.5.14) @@ -135,6 +141,10 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -304,6 +314,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -557,89 +571,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -664,6 +694,14 @@ packages: cpu: [x64] os: [win32] + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -704,24 +742,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.0.0': resolution: {integrity: sha512-xzgl7c7BVk4+7PDWldU+On2nlwnGgFqJ1siWp3/8S0KBBLCjonB6zwJYPtl4MUY7YZJrzzumdUpUoquu5zk8vg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.0.0': resolution: {integrity: sha512-sdyOg4cbiCw7YUr0F/7ya42oiVBXLD21EYkSwN+PhE4csJH4MSXUsYyslliiiBwkM+KsuQH/y9wuxVz6s7Nstg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.0.0': resolution: {integrity: sha512-IAXv3OBYqVaNOgyd3kxR4L3msuhmSy1bcchPHxDOjypG33i2yDWvGBwFD94OuuTjjTt/7cuIKtAmoOOml6kfbg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.0.0': resolution: {integrity: sha512-bmo3ncIJKUS9PWK1JD9pEVv0yuvp1KPuOsyJTHXTv8KDrEmgV/K+U0C75rl9rhIaODcS7JEb6/7eJhdwXI0XmA==} @@ -738,6 +780,10 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@playwright/test@1.59.1': resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} @@ -780,66 +826,79 @@ packages: resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.3': resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.3': resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.3': resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.3': resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.3': resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.3': resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.3': resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.3': resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.3': resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.3': resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.3': resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.3': resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.3': resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} @@ -912,24 +971,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.0': resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.0': resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.0': resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.0': resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} @@ -1033,6 +1096,15 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} + peerDependencies: + '@vitest/browser': 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -1084,10 +1156,22 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -1099,6 +1183,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -1113,6 +1200,9 @@ packages: avvio@9.2.0: resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -1125,6 +1215,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} @@ -1166,6 +1259,13 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -1177,6 +1277,10 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -1241,12 +1345,21 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} electron-to-chromium@1.5.353: resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enhanced-resolve@5.21.2: resolution: {integrity: sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==} engines: {node: '>=10.13.0'} @@ -1328,6 +1441,10 @@ packages: resolution: {integrity: sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==} engines: {node: '>=20'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -1351,6 +1468,11 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -1358,10 +1480,17 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -1397,6 +1526,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -1409,12 +1542,34 @@ packages: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + it-pushable@1.4.2: resolution: {integrity: sha512-vVPu0CGRsTI8eCfhMknA7KIBqqGFolbRx+1mbQ6XuZ7YCz995Qj7L4XUviwClFunisDq96FdxzF5FnAbw15afg==} it-to-stream@1.0.0: resolution: {integrity: sha512-pLULMZMAB/+vbdvbZtebC0nWBTbG581lk6w8P7DfIIIKUfa8FbY7Oi0FxZcFPbxvISs7A9E+cMpLDBc1XhpAOA==} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -1422,6 +1577,9 @@ packages: jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1505,24 +1663,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1586,6 +1748,13 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -1599,6 +1768,10 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -1657,9 +1830,20 @@ packages: p-fifo@1.0.0: resolution: {integrity: sha512-IjoCxXW48tqdtDFz6fqo5q1UfFVjjVZe8TC1QRflvNUJtNfCUhxOUw6MOVZhDPjqhSzc26xKdugsO17gmzd5+A==} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -1837,9 +2021,21 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -1861,9 +2057,25 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -1884,6 +2096,10 @@ packages: babel-plugin-macros: optional: true + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -1897,6 +2113,10 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + thread-stream@4.1.0: resolution: {integrity: sha512-Bw6h2iBDt16v6iHLChBIoVYU8CBo9GPsW8TG7h1hRVhqKhIkH6N8qkxNSmiOZTKsCLPbtWG4ViWLkU6KeKXpig==} engines: {node: '>=20'} @@ -2065,11 +2285,24 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + ws@8.20.0: resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} @@ -2110,6 +2343,11 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -2408,6 +2646,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -2674,6 +2914,17 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.6': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2723,6 +2974,9 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@playwright/test@1.59.1': dependencies: playwright: 1.59.1 @@ -2973,6 +3227,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@25.6.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.12 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@25.6.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.3 @@ -3032,8 +3305,16 @@ snapshots: ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -3042,6 +3323,12 @@ snapshots: assertion-error@2.0.1: {} + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + atomic-sleep@1.0.0: {} autoprefixer@10.5.0(postcss@8.5.14): @@ -3058,12 +3345,18 @@ snapshots: '@fastify/error': 4.2.0 fastq: 1.20.1 + balanced-match@1.0.2: {} + balanced-match@4.0.4: {} base64-js@1.5.1: {} baseline-browser-mapping@2.10.29: {} + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -3105,12 +3398,24 @@ snapshots: clsx@2.1.1: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + content-disposition@1.1.0: {} convert-source-map@2.0.0: {} cookie@1.1.1: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + css.escape@1.5.1: {} cssstyle@4.6.0: @@ -3154,12 +3459,18 @@ snapshots: dotenv@16.6.1: {} + eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 electron-to-chromium@1.5.353: {} + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + enhanced-resolve@5.21.2: dependencies: graceful-fs: 4.2.11 @@ -3274,6 +3585,11 @@ snapshots: fast-querystring: 1.1.2 safe-regex2: 5.1.1 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fraction.js@5.3.4: {} fsevents@2.3.2: @@ -3290,6 +3606,15 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -3298,10 +3623,14 @@ snapshots: graceful-fs@4.2.11: {} + has-flag@4.0.0: {} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -3338,6 +3667,8 @@ snapshots: is-docker@3.0.0: {} + is-fullwidth-code-point@3.0.0: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -3348,6 +3679,29 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + it-pushable@1.4.2: dependencies: fast-fifo: 1.3.2 @@ -3361,10 +3715,18 @@ snapshots: p-fifo: 1.0.0 readable-stream: 3.6.2 + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@2.7.0: {} jose@6.2.3: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -3527,6 +3889,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.0 + mime@3.0.0: {} min-indent@1.0.1: {} @@ -3535,6 +3907,10 @@ snapshots: dependencies: brace-expansion: 5.0.6 + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + minipass@7.1.3: {} ms@2.1.3: {} @@ -3587,10 +3963,19 @@ snapshots: fast-fifo: 1.3.2 p-defer: 3.0.0 + package-json-from-dist@1.0.1: {} + parse5@7.3.0: dependencies: entities: 6.0.1 + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-scurry@2.0.2: dependencies: lru-cache: 11.3.6 @@ -3793,8 +4178,16 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -3809,10 +4202,30 @@ snapshots: std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -3828,6 +4241,10 @@ snapshots: optionalDependencies: '@babel/core': 7.29.0 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + symbol-tree@3.2.4: {} tailwind-merge@3.6.0: {} @@ -3836,6 +4253,12 @@ snapshots: tapable@2.3.3: {} + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 10.5.0 + minimatch: 10.2.5 + thread-stream@4.1.0: dependencies: real-require: 1.0.0 @@ -3990,11 +4413,27 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + ws@8.20.0: {} wsl-utils@0.1.0: diff --git a/dashboard/web/src/app/globals.css b/dashboard/web/src/app/globals.css index 3993748..dd3e4e8 100644 --- a/dashboard/web/src/app/globals.css +++ b/dashboard/web/src/app/globals.css @@ -3,3 +3,8 @@ @tailwind base; @tailwind components; @tailwind utilities; + +html, +body { + font-family: var(--ml-font-body), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} diff --git a/dashboard/web/src/app/hermes/agents/page.tsx b/dashboard/web/src/app/hermes/agents/page.tsx index f18b700..53b808c 100644 --- a/dashboard/web/src/app/hermes/agents/page.tsx +++ b/dashboard/web/src/app/hermes/agents/page.tsx @@ -33,7 +33,7 @@ export default function HermesAgentsPage() {

{agent.name}

{agent.type} · {agent.callsToday} calls today

- {agent.status} + {agent.status}
Last success: {agent.lastSuccessAt ? new Date(agent.lastSuccessAt).toLocaleString() : '—'}
diff --git a/dashboard/web/src/app/hermes/page.tsx b/dashboard/web/src/app/hermes/page.tsx index 6615868..cb5c2af 100644 --- a/dashboard/web/src/app/hermes/page.tsx +++ b/dashboard/web/src/app/hermes/page.tsx @@ -4,6 +4,7 @@ import Link from 'next/link'; import { ArrowRight, BadgeCheck, Bot, CheckCircle2, Clock3, LayoutDashboard, OctagonAlert, Rocket, ShieldAlert, Sparkles, TriangleAlert } from 'lucide-react'; import { Badge, Button } from '@/components/ui/Primitives'; import { HermesShell, MetricCard, SectionCard } from '@/components/hermes-shell'; +import { HermesOpsPanel } from '@/components/hermes-ops-panel'; import { getHermesAgents, getHermesOverview, @@ -22,14 +23,14 @@ const fmtDate = new Intl.DateTimeFormat('en', { minute: '2-digit', }); -const statusTone: Record = { +const statusTone: Record = { running: 'info', - idle: 'default', + idle: 'neutral', degraded: 'warning', - error: 'danger', - queued: 'default', + error: 'error', + queued: 'neutral', blocked: 'warning', - failed: 'danger', + failed: 'error', completed: 'success', }; @@ -38,7 +39,7 @@ function taskStatusLabel(task: HermesTask) { } function getTaskTone(task: HermesTask) { - return statusTone[task.status] ?? 'default'; + return statusTone[task.status] ?? 'neutral'; } function ProductMiniCard({ product }: { product: HermesProduct }) { @@ -117,6 +118,8 @@ export default function HermesMissionControlPage() { } helpText={`${overview.productsTouchedRecently} products touched in the last 14 days`} /> + +
View all tasks }>
@@ -162,7 +165,7 @@ export default function HermesMissionControlPage() { {task.title}

{task.blockerReason ?? task.error ?? task.nextAction}

- {task.status} + {task.status}
))} @@ -256,7 +259,7 @@ export default function HermesMissionControlPage() {

{agent.type} · {agent.callsToday} calls today

{agent.configIssue ?

{agent.configIssue}

: null} - {agent.status} + {agent.status} ))} diff --git a/dashboard/web/src/app/hermes/products/page.tsx b/dashboard/web/src/app/hermes/products/page.tsx index 856e706..781a4ac 100644 --- a/dashboard/web/src/app/hermes/products/page.tsx +++ b/dashboard/web/src/app/hermes/products/page.tsx @@ -20,7 +20,7 @@ function getHealthTone(score: number) { if (score >= 85) return 'success'; if (score >= 70) return 'info'; if (score >= 55) return 'warning'; - return 'danger'; + return 'error'; } function ProductCard({ product }: { product: HermesProduct }) { diff --git a/dashboard/web/src/app/hermes/tasks/[id]/page.tsx b/dashboard/web/src/app/hermes/tasks/[id]/page.tsx index 0a9db2c..40e7d16 100644 --- a/dashboard/web/src/app/hermes/tasks/[id]/page.tsx +++ b/dashboard/web/src/app/hermes/tasks/[id]/page.tsx @@ -12,7 +12,7 @@ function levelTone(level: 'debug' | 'info' | 'warn' | 'error' | 'success') { switch (level) { case 'success': return 'success'; case 'warn': return 'warning'; - case 'error': return 'danger'; + case 'error': return 'error'; case 'debug': return 'neutral'; default: return 'info'; } @@ -58,7 +58,7 @@ export default function HermesTaskDetailPage({ params }: { params: { id: string
- {task.status} + {task.status} {task.source}
diff --git a/dashboard/web/src/app/hermes/tasks/page.tsx b/dashboard/web/src/app/hermes/tasks/page.tsx index 033e082..42ca226 100644 --- a/dashboard/web/src/app/hermes/tasks/page.tsx +++ b/dashboard/web/src/app/hermes/tasks/page.tsx @@ -146,8 +146,8 @@ export default function HermesTaskLedgerPage() {
{product?.name ?? 'Unknown'} - {task.status} - {task.priority} + {task.status} + {task.priority} {task.type} {task.source} {prettyDate(task.createdAt)} diff --git a/dashboard/web/src/app/layout.tsx b/dashboard/web/src/app/layout.tsx index 2646ccc..6c71863 100644 --- a/dashboard/web/src/app/layout.tsx +++ b/dashboard/web/src/app/layout.tsx @@ -1,11 +1,8 @@ import type { Metadata, Viewport } from 'next'; -import { Inter } from 'next/font/google'; import './globals.css'; import { AuthProvider } from '@/lib/auth'; import { ErrorBoundary } from '@/components/error-boundary'; -const inter = Inter({ subsets: ['latin'] }); - export const metadata: Metadata = { title: 'ByteLyst DevOps', description: 'Internal DevOps dashboard for deployment orchestration', @@ -31,7 +28,7 @@ export default function RootLayout({ }>) { return ( - + + {label} + {value} +
+ ); +} + +function InstanceCard({ instance }: { instance: HermesOpsInstance }) { + const score = [ + instance.gateway.active, + instance.gateway.enabled, + instance.dashboard.active, + instance.backup.timer.active, + instance.backup.repo.clean, + instance.google.workspaceToken, + ].filter(Boolean).length; + + return ( +
+
+
+
+ +

{instance.label}

+
+

{instance.hermesHome}

+
+ = 4 ? 'warning' : 'error'}>{score}/6 healthy +
+ +
+ + + + + +
+ +
+
+
+ + Backup repo +
+

HEAD {instance.backup.repo.head ?? 'unknown'}

+

Last commit {formatDate(instance.backup.repo.lastCommitAt)}

+

{instance.backup.repo.clean ? 'Clean working tree' : 'Uncommitted changes present'}

+
+
+
+ + Restore payload +
+

{instance.backup.restoredFileCount ?? 'unknown'} tracked files

+

{instance.backup.restoredCronJobs ?? 'unknown'} cron job definitions

+

{instance.backup.repo.size ?? 'size unknown'}

+
+
+ +
+
+ ); +} + +export function HermesOpsPanel() { + const [snapshot, setSnapshot] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const load = async () => { + setLoading(true); + setError(null); + try { + setSnapshot(await api.getHermesOps()); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unable to load Hermes operations status'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void load(); + const id = window.setInterval(() => void load(), 60_000); + return () => window.clearInterval(id); + }, []); + + const allHealthy = useMemo(() => snapshot ? snapshot.warnings.length === 0 : false, [snapshot]); + + return ( + + {snapshot ? {allHealthy ? 'All green' : `${snapshot.warnings.length} warning(s)`} : null} + +
+ )} + > + {error ? ( +
+ {error} +
+ ) : null} + + {snapshot ? ( +
+
+
+
+ + Tailscale IP +
+

{snapshot.tailscaleIp ?? 'unknown'}

+
+
+
+ + Emergency Drive +
+

{snapshot.emergencyDriveUpload.active ? 'active' : 'inactive'}

+
+
+
+ + Next Drive bundle +
+

{snapshot.emergencyDriveUpload.nextRun ?? 'unknown'}

+
+
+
+ + Generated +
+

{formatDate(snapshot.generatedAt)}

+
+
+ + {snapshot.warnings.length ? ( +
+
+ + Recovery warnings +
+
+ {snapshot.warnings.map((warning) => ( +
{warning}
+ ))} +
+
+ ) : ( +
+ + Vijay and Bheem recovery paths are healthy. +
+ )} + +
+ {snapshot.instances.map((instance) => ( + + ))} +
+ +
+ Disaster recovery details live in{' '} + Hermes settings + {' '}and the tracked runbook in `docs/hermes-disaster-recovery.md`. +
+
+ ) : ( +
+ Loading live Hermes operations status... +
+ )} + + ); +} diff --git a/dashboard/web/src/components/ui/Primitives.tsx b/dashboard/web/src/components/ui/Primitives.tsx index 67039bb..216bca2 100644 --- a/dashboard/web/src/components/ui/Primitives.tsx +++ b/dashboard/web/src/components/ui/Primitives.tsx @@ -28,8 +28,9 @@ export const Button = React.forwardRef( const classes = cn(baseStyles, variantStyles[variant], sizeStyles[size], className); if (asChild && React.isValidElement(children)) { - return React.cloneElement(children as React.ReactElement<{ className?: string }>, { - className: cn(children.props.className, classes), + const child = children as React.ReactElement<{ className?: string }>; + return React.cloneElement(child, { + className: cn(child.props.className, classes), }); } diff --git a/dashboard/web/src/lib/api.ts b/dashboard/web/src/lib/api.ts index 9e76f48..0601ed9 100644 --- a/dashboard/web/src/lib/api.ts +++ b/dashboard/web/src/lib/api.ts @@ -56,6 +56,56 @@ export interface EnvVar { updatedAt: string; } +export interface HermesOpsTimer { + name: string; + active: boolean; + nextRun: string | null; + lastRun: string | null; +} + +export interface HermesOpsRepo { + path: string; + branch: string | null; + clean: boolean; + head: string | null; + lastCommitAt: string | null; + size: string | null; +} + +export interface HermesOpsInstance { + id: 'vijay' | 'bheem'; + label: string; + hermesHome: string; + gateway: { + service: string; + active: boolean; + enabled: boolean; + }; + dashboard: { + service: string; + active: boolean; + url: string; + }; + backup: { + timer: HermesOpsTimer; + repo: HermesOpsRepo; + restoredFileCount: number | null; + restoredCronJobs: number | null; + }; + google: { + workspaceToken: boolean; + driveFolder: string; + }; +} + +export interface HermesOpsSnapshot { + generatedAt: string; + tailscaleIp: string | null; + emergencyDriveUpload: HermesOpsTimer; + instances: HermesOpsInstance[]; + warnings: string[]; +} + let csrfToken: string | null = null; let csrfTokenExpiresAt: number = 0; @@ -208,6 +258,9 @@ export const api = { apiRequest(`/api/health/${serviceId}`), clearHealthCache: () => apiRequest<{ message: string }>('/api/health/cache', { method: 'DELETE' }), + // Hermes operations + getHermesOps: () => apiRequest('/api/hermes/ops'), + // Seed seedServices: () => apiRequest<{ message: string }>('/api/seed', { method: 'POST' }),