This commit is contained in:
2026-05-12 19:25:14 +02:00
commit 0f3173d93e
93 changed files with 11865 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
node_modules/
dist/
.vite/
.git/
.idea/
.vscode/
*.log
pnpm-lock.yaml
package-lock.json
yarn.lock
+4
View File
@@ -0,0 +1,4 @@
# Used by Vite during `pnpm dev`.
# Where to proxy /api/* during development. Default: the Rust binary running
# locally on the default BIND_ADDR.
VITE_API_PROXY_TARGET=http://localhost:8443
+25
View File
@@ -0,0 +1,25 @@
node_modules/
dist/
.vite/
.tanstack/
.tsbuildinfo
.env
.env.local
.env.*.local
*.log
# Claude / editor / OS noise
.claude/
.idea/
.vscode/
*.swp
.DS_Store
# Lockfiles from other package managers — bun.lock is the source of truth
pnpm-lock.yaml
package-lock.json
yarn.lock
# Generated client schema — leave the latest in repo for reproducible builds,
# regenerate via `pnpm gen:api`.
# src/api/schema.d.ts
+19
View File
@@ -0,0 +1,19 @@
# syntax=docker/dockerfile:1.7
FROM oven/bun:1-alpine AS builder
WORKDIR /app
COPY package.json bun.lock* bun.lockb* ./
RUN bun install --frozen-lockfile
COPY tsconfig*.json vite.config.ts index.html ./
COPY openapi.json ./
COPY public ./public
COPY src ./src
RUN bun run gen:api && bun run build
# Runtime image is intentionally generic — the nginx config is volume-mounted
# from ../deploy/nginx.conf by compose so ops can iterate without rebuilds.
FROM nginx:1.27-alpine AS runtime
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
+88
View File
@@ -0,0 +1,88 @@
# smgw-pki-console
Web-Konsole für den `smgw-pki-automator`. React + TypeScript + Vite. Typisierter
API-Client wird aus der utoipa-generierten OpenAPI-Spezifikation gebaut.
## Stack
- React 19 + TypeScript (strict) + Vite 6
- TanStack Router (file-based) + TanStack Query
- Tailwind v4 + shadcn-Stil Primitives auf Radix
- `openapi-typescript` + `openapi-fetch` für typisierten Backend-Client
- Sonner für Toasts, Lucide-Icons
## Layout
```
src/
├── api/schema.d.ts Generiert via `just gen-api` (oder `bun run gen:api`)
├── components/
│ ├── ui/ Button, Card, Badge, Table, Dialog …
│ ├── layout/ Sidebar, Topbar, App-Shell
│ └── state-badge.tsx
├── lib/
│ ├── api.ts openapi-fetch Client
│ ├── format.ts Datum/Serial-Formatter
│ └── utils.ts cn()
├── routes/ TanStack Router (file-based)
│ ├── __root.tsx
│ ├── login.tsx
│ ├── _app.tsx Auth-Guard + Shell
│ ├── _app.index.tsx Dashboard
│ ├── _app.certificates.tsx
│ ├── _app.configuration.tsx
│ └── _app.iconfig.tsx
├── index.css Tailwind Theme (light, BSI-Ton)
└── main.tsx
```
## Lokale Entwicklung
Bun ist die Standard-Toolchain (ein Binary, kein Approval-Tanz für Build-Skripte). Andere Manager (pnpm, npm) funktionieren grundsätzlich auch.
```bash
bun install
bun run gen:api # liest ./openapi.json
bun run dev # http://localhost:5173, /api wird zu localhost:8443 gepro­xied
```
Backend separat aus `../backend`:
```bash
DEV_AUTH=1 CORS_ALLOW_ORIGIN=http://localhost:5173 cargo run
```
## OpenAPI-Client
```bash
bun run gen:api # statisch aus ./openapi.json
bun run gen:api:live # gegen laufendes Backend (localhost:8443)
```
OpenAPI selbst stammt aus dem Rust-Code via `utoipa-axum`. Frischen Snapshot in
diesem Verzeichnis ablegen:
```bash
cd ../backend && cargo run -- --emit-openapi > ../frontend/openapi.json
```
## Docker (über Repo-Root)
```bash
just up # beide Container bauen + starten
just down
just logs frontend
```
- Frontend: <http://localhost:8080>
- Backend (intern): `backend:8443`
- `deploy/nginx.conf` wird zur Laufzeit in den Frontend-Container gemountet — Edit + `just nginx-reload` ohne Image-Rebuild.
- mTLS-Termination findet **vor** nginx statt; der Proxy setzt `X-Forwarded-Cert-Subject`. Im Lab läuft das Backend mit `DEV_AUTH=1` und akzeptiert ein `dev_subject` im Login-Body.
## Was noch fehlt (TODOs)
- PEM-Anzeige im Certificate-Detail-Drawer (Backend liefert PEM noch nicht
separat).
- SSE-Stream für Live-Scheduler-Logs (Endpoint `/api/scheduler/stream`).
- Vollständige iconfig-Profil-Felder, sobald `InitialConfigBuilder` real ist.
- Audit-Log-Seite.
+745
View File
@@ -0,0 +1,745 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "smgw-pki-console",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.5",
"@tanstack/react-query": "^5.62.7",
"@tanstack/react-router": "^1.93.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"geist": "^1.3.1",
"lucide-react": "^0.468.0",
"openapi-fetch": "^0.13.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sonner": "^1.7.1",
"tailwind-merge": "^2.5.5",
"zod": "^3.24.1",
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@tanstack/router-cli": "^1.166.43",
"@tanstack/router-plugin": "^1.93.0",
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.3.4",
"openapi-typescript": "^7.4.4",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.2",
"vite": "^6.0.5",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
"@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="],
"@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="],
"@redocly/openapi-core": ["@redocly/openapi-core@1.34.14", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.3", "", { "os": "android", "cpu": "arm" }, "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.3", "", { "os": "android", "cpu": "arm64" }, "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.3", "", { "os": "none", "cpu": "arm64" }, "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="],
"@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="],
"@tanstack/query-core": ["@tanstack/query-core@5.100.10", "", {}, "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w=="],
"@tanstack/react-query": ["@tanstack/react-query@5.100.10", "", { "dependencies": { "@tanstack/query-core": "5.100.10" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q=="],
"@tanstack/react-router": ["@tanstack/react-router@1.169.2", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.169.2", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-OJM7Kguc7ERnweaNRWsyWgIKcl3z23rD1B4jaxjzd9RGdnzpt2HfrWa9rggbT0Hfzhfo4D2ZmsfoTme035tniQ=="],
"@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="],
"@tanstack/router-cli": ["@tanstack/router-cli@1.166.43", "", { "dependencies": { "@tanstack/router-generator": "1.166.42", "chokidar": "^3.6.0", "yargs": "^17.7.2" }, "bin": { "tsr": "bin/tsr.cjs" } }, "sha512-XvKFA47F5KjL0R8PzUdkBQrVDPjSzE7FgWeKnYLVRGytDTlZOJhgVzoznITdiAsNe9KPe93xHE1z5h80hhOGWg=="],
"@tanstack/router-core": ["@tanstack/router-core@1.169.2", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-5sm0DJF1A7Mz+9gy4Gz/lLovNailK3yot4vYvz9MkBUPw26uLnhQiR8hSCYxucjE0wD6Mdlc5l+Z0/XTlZ7xHw=="],
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.42", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.169.2", "@tanstack/router-utils": "1.161.8", "@tanstack/virtual-file-routes": "1.161.7", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^3.24.2" } }, "sha512-2qBWC0t78r6b3vI+AbnvCZcFAvbYBDlLuWZrTjQbcjUmwG3qyeQp983tJyDuj9wb5//adG1tgAGXZkJ3aDwdBg=="],
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.35", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.169.2", "@tanstack/router-generator": "1.166.42", "@tanstack/router-utils": "1.161.8", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^3.0.0", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.169.2", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-UAScU5VAzLYVY4FML/Cbc5S5TucT4I8Ata05yozGOe4ZfepTKRffA5xWLtD2N+ov5svdv0KTX/kqlZnYPe28mA=="],
"@tanstack/router-utils": ["@tanstack/router-utils@1.161.8", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-xyiLWEKjfBAVhauDSSjXxyf7s8elU6SM+V050sbkofvGmIIvkwPFtDsX7Gvwh14kBd6iCwAT+RiPvXTxAptY0Q=="],
"@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"ansis": ["ansis@4.3.0", "", {}, "sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.29", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
"change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
"electron-to-chromium": ["electron-to-chromium@1.5.353", "", {}, "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"enhanced-resolve": ["enhanced-resolve@5.21.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"geist": ["geist@1.7.0", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"isbot": ["isbot@5.1.40", "", {}, "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.468.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
"next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="],
"node-releases": ["node-releases@2.0.44", "", {}, "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"openapi-fetch": ["openapi-fetch@0.13.8", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.15" } }, "sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ=="],
"openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="],
"openapi-typescript-helpers": ["openapi-typescript-helpers@0.0.15", "", {}, "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw=="],
"parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
"react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"rollup": ["rollup@4.60.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.3", "@rollup/rollup-android-arm64": "4.60.3", "@rollup/rollup-darwin-arm64": "4.60.3", "@rollup/rollup-darwin-x64": "4.60.3", "@rollup/rollup-freebsd-arm64": "4.60.3", "@rollup/rollup-freebsd-x64": "4.60.3", "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", "@rollup/rollup-linux-arm-musleabihf": "4.60.3", "@rollup/rollup-linux-arm64-gnu": "4.60.3", "@rollup/rollup-linux-arm64-musl": "4.60.3", "@rollup/rollup-linux-loong64-gnu": "4.60.3", "@rollup/rollup-linux-loong64-musl": "4.60.3", "@rollup/rollup-linux-ppc64-gnu": "4.60.3", "@rollup/rollup-linux-ppc64-musl": "4.60.3", "@rollup/rollup-linux-riscv64-gnu": "4.60.3", "@rollup/rollup-linux-riscv64-musl": "4.60.3", "@rollup/rollup-linux-s390x-gnu": "4.60.3", "@rollup/rollup-linux-x64-gnu": "4.60.3", "@rollup/rollup-linux-x64-musl": "4.60.3", "@rollup/rollup-openbsd-x64": "4.60.3", "@rollup/rollup-openharmony-arm64": "4.60.3", "@rollup/rollup-win32-arm64-msvc": "4.60.3", "@rollup/rollup-win32-ia32-msvc": "4.60.3", "@rollup/rollup-win32-x64-gnu": "4.60.3", "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="],
"seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
"tailwind-merge": ["tailwind-merge@2.6.1", "", {}, "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ=="],
"tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"sharp/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
}
}
+20
View File
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700&family=Instrument+Serif:ital@0;1&display=swap"
/>
<title>SMGW PKI · Console</title>
</head>
<body class="bg-canvas text-ink antialiased">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+949
View File
@@ -0,0 +1,949 @@
{
"openapi": "3.1.0",
"info": {
"title": "smgw-pki-automator",
"description": "Control + observation surface for the SMGW PKI automation tool. Test/lab use only.",
"license": {
"name": ""
},
"version": "0.1.0"
},
"paths": {
"/alerts": {
"get": {
"tags": [
"alerts"
],
"operationId": "list_alerts",
"responses": {
"200": {
"description": "Recent alerts",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AlertListResponse"
}
}
}
}
}
}
},
"/alerts/test": {
"post": {
"tags": [
"alerts"
],
"operationId": "send_test_alert",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestAlertRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Sent (or stub-logged)"
}
}
}
},
"/auth/me": {
"get": {
"tags": [
"auth"
],
"summary": "Inspect the current session.",
"operationId": "whoami",
"responses": {
"200": {
"description": "Current session",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionResponse"
}
}
}
},
"401": {
"description": "No active session",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/auth/session": {
"post": {
"tags": [
"auth"
],
"summary": "Exchange mTLS client cert (passed via `X-Forwarded-Cert-Subject`) for a\nserver-issued session cookie. The reverse proxy terminating mTLS is the\ntrust anchor.",
"operationId": "create_session",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Session issued",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionResponse"
}
}
}
},
"403": {
"description": "No client cert subject",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
},
"delete": {
"tags": [
"auth"
],
"summary": "Revoke the current session and clear cookie.",
"operationId": "end_session",
"responses": {
"204": {
"description": "Session ended"
}
}
}
},
"/certs": {
"get": {
"tags": [
"certs"
],
"summary": "List all known end-entity certificates with derived state.",
"operationId": "list_certificates",
"responses": {
"200": {
"description": "Certificates",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CertListResponse"
}
}
}
}
}
}
},
"/certs/{gateway_id}/{usage}/renew": {
"post": {
"tags": [
"certs"
],
"summary": "Trigger an out-of-band renewal for a specific (gateway, usage) pair.\nReturns the SOAP `messageID` so the caller can correlate the async callback.",
"operationId": "renew_certificate",
"parameters": [
{
"name": "gateway_id",
"in": "path",
"description": "Gateway identifier",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "usage",
"in": "path",
"description": "Certificate usage",
"required": true,
"schema": {
"$ref": "#/components/schemas/CertificateUsageDto"
}
}
],
"responses": {
"202": {
"description": "Renewal accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RenewAccepted"
}
}
}
},
"501": {
"description": "Sub-CA adapter not implemented yet",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/config": {
"get": {
"tags": [
"config"
],
"operationId": "get_config",
"responses": {
"200": {
"description": "Current runtime config",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfigView"
}
}
}
}
}
},
"put": {
"tags": [
"config"
],
"operationId": "update_config",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfigUpdate"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Updated runtime config",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfigView"
}
}
}
}
}
}
},
"/gateways": {
"get": {
"tags": [
"gateways"
],
"operationId": "list_gateways",
"responses": {
"200": {
"description": "Gateways",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GatewayListResponse"
}
}
}
}
}
}
},
"/iconfig/build": {
"post": {
"tags": [
"iconfig"
],
"summary": "Build, sign via HSM, and stream back `iconfig.tar`.",
"operationId": "build_iconfig",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IconfigRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "iconfig.tar",
"content": {
"application/x-tar": {}
}
},
"501": {
"description": "HSM signature not implemented",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/iconfig/preview": {
"post": {
"tags": [
"iconfig"
],
"summary": "Render the unsigned `iconfig.xml` for review. Does not touch the HSM.",
"operationId": "preview_iconfig",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IconfigRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Preview XML",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IconfigPreview"
}
}
}
}
}
}
},
"/scheduler": {
"get": {
"tags": [
"scheduler"
],
"operationId": "get_status",
"responses": {
"200": {
"description": "Scheduler state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SchedulerState"
}
}
}
}
}
}
},
"/scheduler/pause": {
"post": {
"tags": [
"scheduler"
],
"operationId": "set_paused",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PauseRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Pause state updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SchedulerState"
}
}
}
}
}
}
},
"/scheduler/trigger": {
"post": {
"tags": [
"scheduler"
],
"summary": "Run renewal once, out of band. Honours the same overlap-lock as the cron job.",
"operationId": "trigger_run",
"responses": {
"202": {
"description": "Run accepted"
},
"409": {
"description": "Run already in progress",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"AlertEntry": {
"type": "object",
"required": [
"at",
"severity",
"subject",
"body"
],
"properties": {
"at": {
"type": "string",
"format": "date-time"
},
"body": {
"type": "string"
},
"severity": {
"$ref": "#/components/schemas/AlertSeverity"
},
"subject": {
"type": "string"
}
}
},
"AlertListResponse": {
"type": "object",
"required": [
"items"
],
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AlertEntry"
}
}
}
},
"AlertSeverity": {
"type": "string",
"enum": [
"info",
"warning",
"error"
]
},
"ApiError": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
}
}
},
"CertListResponse": {
"type": "object",
"required": [
"items"
],
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CertificateDto"
}
}
}
},
"CertState": {
"type": "string",
"enum": [
"valid",
"expiring",
"expired"
]
},
"CertificateDto": {
"type": "object",
"required": [
"gateway_id",
"serial",
"usage",
"not_before",
"not_after",
"days_to_expiry",
"state"
],
"properties": {
"days_to_expiry": {
"type": "integer",
"format": "int64"
},
"gateway_id": {
"type": "string"
},
"not_after": {
"type": "string",
"format": "date-time"
},
"not_before": {
"type": "string",
"format": "date-time"
},
"serial": {
"type": "string"
},
"state": {
"$ref": "#/components/schemas/CertState"
},
"usage": {
"$ref": "#/components/schemas/CertificateUsageDto"
}
}
},
"CertificateUsageDto": {
"type": "string",
"enum": [
"tls",
"signature",
"encryption"
]
},
"ConfigUpdate": {
"type": "object",
"properties": {
"cron_schedule": {
"type": [
"string",
"null"
]
},
"days_window": {
"type": [
"integer",
"null"
],
"format": "int32",
"minimum": 0
},
"hsm": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/HsmConfig"
}
]
},
"smtp": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/SmtpConfig"
}
]
},
"sub_ca": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/SubCaConfig"
}
]
}
}
},
"ConfigView": {
"type": "object",
"required": [
"config",
"restart_required_fields"
],
"properties": {
"config": {
"$ref": "#/components/schemas/RuntimeConfig"
},
"restart_required_fields": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"GatewayDto": {
"type": "object",
"required": [
"id",
"serial_number",
"admin_key_label"
],
"properties": {
"admin_key_label": {
"type": "string"
},
"id": {
"type": "string"
},
"serial_number": {
"type": "string"
}
}
},
"GatewayListResponse": {
"type": "object",
"required": [
"items"
],
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GatewayDto"
}
}
}
},
"HsmConfig": {
"type": "object",
"required": [
"module_path",
"pin_env_var"
],
"properties": {
"module_path": {
"type": "string"
},
"pin_env_var": {
"type": "string"
},
"slot": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
}
},
"IconfigPreview": {
"type": "object",
"required": [
"xml"
],
"properties": {
"xml": {
"type": "string"
}
}
},
"IconfigRequest": {
"type": "object",
"required": [
"gateway_id",
"admin_key_label",
"profile"
],
"properties": {
"admin_key_label": {
"type": "string"
},
"extras": {},
"gateway_id": {
"type": "string"
},
"profile": {
"type": "string"
}
}
},
"LoginRequest": {
"type": "object",
"properties": {
"dev_subject": {
"type": [
"string",
"null"
],
"description": "Optional fallback subject for dev mode when no mTLS header is present."
}
}
},
"PauseRequest": {
"type": "object",
"required": [
"paused"
],
"properties": {
"paused": {
"type": "boolean"
}
}
},
"RenewAccepted": {
"type": "object",
"required": [
"message_id"
],
"properties": {
"message_id": {
"type": "string"
}
}
},
"RuntimeConfig": {
"type": "object",
"description": "Mutable runtime config. Seeded from env on boot; UI may override at runtime\nfor fields flagged `hot_reload`. Restart-only fields (BIND_ADDR) are read\nbut cannot be applied without restart.",
"required": [
"bind_addr",
"cron_schedule",
"days_window",
"database_url",
"sub_ca",
"smtp",
"hsm"
],
"properties": {
"bind_addr": {
"type": "string"
},
"cron_schedule": {
"type": "string"
},
"database_url": {
"type": "string"
},
"days_window": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"hsm": {
"$ref": "#/components/schemas/HsmConfig"
},
"smtp": {
"$ref": "#/components/schemas/SmtpConfig"
},
"sub_ca": {
"$ref": "#/components/schemas/SubCaConfig"
}
}
},
"SchedulerState": {
"type": "object",
"required": [
"cron_schedule",
"days_window",
"paused"
],
"properties": {
"cron_schedule": {
"type": "string"
},
"days_window": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"last_error": {
"type": [
"string",
"null"
]
},
"last_handled": {
"type": [
"integer",
"null"
],
"minimum": 0
},
"last_run_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"last_run_ok": {
"type": [
"boolean",
"null"
]
},
"paused": {
"type": "boolean"
}
}
},
"SessionResponse": {
"type": "object",
"required": [
"subject",
"expires_at"
],
"properties": {
"expires_at": {
"type": "string",
"format": "date-time"
},
"subject": {
"type": "string"
}
}
},
"SmtpConfig": {
"type": "object",
"required": [
"host",
"port",
"from",
"to",
"starttls"
],
"properties": {
"from": {
"type": "string"
},
"host": {
"type": "string"
},
"port": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"starttls": {
"type": "boolean"
},
"to": {
"type": "string"
}
}
},
"SubCaConfig": {
"type": "object",
"required": [
"endpoint"
],
"properties": {
"ca_bundle_path": {
"type": [
"string",
"null"
]
},
"client_cert_path": {
"type": [
"string",
"null"
]
},
"client_key_path": {
"type": [
"string",
"null"
]
},
"endpoint": {
"type": "string"
}
}
},
"TestAlertRequest": {
"type": "object",
"properties": {
"body": {
"type": [
"string",
"null"
]
},
"subject": {
"type": [
"string",
"null"
]
}
}
}
}
},
"tags": [
{
"name": "auth",
"description": "mTLS-bridged session management"
},
{
"name": "certs",
"description": "Certificate lifecycle"
},
{
"name": "gateways",
"description": "Smart Meter Gateways"
},
{
"name": "config",
"description": "Runtime configuration"
},
{
"name": "scheduler",
"description": "Renewal scheduler"
},
{
"name": "iconfig",
"description": "BSI TR-03109-1 initial config"
},
{
"name": "alerts",
"description": "Operator alerts"
}
]
}
+52
View File
@@ -0,0 +1,52 @@
{
"name": "smgw-pki-console",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview --host",
"typecheck": "tsc -b --noEmit",
"lint": "eslint .",
"gen:api": "openapi-typescript ./openapi.json -o ./src/api/schema.d.ts",
"gen:api:live": "openapi-typescript http://localhost:8443/api/openapi.json -o ./src/api/schema.d.ts"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.5",
"@tanstack/react-query": "^5.62.7",
"@tanstack/react-router": "^1.93.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"geist": "^1.3.1",
"lucide-react": "^0.468.0",
"openapi-fetch": "^0.13.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sonner": "^1.7.1",
"tailwind-merge": "^2.5.5",
"zod": "^3.24.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@tanstack/router-cli": "^1.166.43",
"@tanstack/router-plugin": "^1.93.0",
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.3.4",
"openapi-typescript": "^7.4.4",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.2",
"vite": "^6.0.5"
}
}
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#0c0c0d" />
<path d="M16 6 L24 10 V18 C24 22 20.5 25 16 26 C11.5 25 8 22 8 18 V10 Z"
fill="none" stroke="#fafaf7" stroke-width="1.5" />
<circle cx="16" cy="16" r="2.2" fill="#fafaf7" />
<path d="M16 18 V22" stroke="#fafaf7" stroke-width="1.5" stroke-linecap="round" />
</svg>

After

Width:  |  Height:  |  Size: 395 B

+740
View File
@@ -0,0 +1,740 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/alerts": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["list_alerts"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/alerts/test": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["send_test_alert"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/auth/me": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Inspect the current session. */
get: operations["whoami"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/auth/session": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Exchange mTLS client cert (passed via `X-Forwarded-Cert-Subject`) for a
* server-issued session cookie. The reverse proxy terminating mTLS is the
* trust anchor.
*/
post: operations["create_session"];
/** Revoke the current session and clear cookie. */
delete: operations["end_session"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/certs": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** List all known end-entity certificates with derived state. */
get: operations["list_certificates"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/certs/{gateway_id}/{usage}/renew": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Trigger an out-of-band renewal for a specific (gateway, usage) pair.
* Returns the SOAP `messageID` so the caller can correlate the async callback.
*/
post: operations["renew_certificate"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/config": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["get_config"];
put: operations["update_config"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/gateways": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["list_gateways"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/iconfig/build": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Build, sign via HSM, and stream back `iconfig.tar`. */
post: operations["build_iconfig"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/iconfig/preview": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Render the unsigned `iconfig.xml` for review. Does not touch the HSM. */
post: operations["preview_iconfig"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/scheduler": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["get_status"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/scheduler/pause": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["set_paused"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/scheduler/trigger": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Run renewal once, out of band. Honours the same overlap-lock as the cron job. */
post: operations["trigger_run"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
AlertEntry: {
/** Format: date-time */
at: string;
body: string;
severity: components["schemas"]["AlertSeverity"];
subject: string;
};
AlertListResponse: {
items: components["schemas"]["AlertEntry"][];
};
/** @enum {string} */
AlertSeverity: "info" | "warning" | "error";
ApiError: {
code: string;
message: string;
};
CertListResponse: {
items: components["schemas"]["CertificateDto"][];
};
/** @enum {string} */
CertState: "valid" | "expiring" | "expired";
CertificateDto: {
/** Format: int64 */
days_to_expiry: number;
gateway_id: string;
/** Format: date-time */
not_after: string;
/** Format: date-time */
not_before: string;
serial: string;
state: components["schemas"]["CertState"];
usage: components["schemas"]["CertificateUsageDto"];
};
/** @enum {string} */
CertificateUsageDto: "tls" | "signature" | "encryption";
ConfigUpdate: {
cron_schedule?: string | null;
/** Format: int32 */
days_window?: number | null;
hsm?: null | components["schemas"]["HsmConfig"];
smtp?: null | components["schemas"]["SmtpConfig"];
sub_ca?: null | components["schemas"]["SubCaConfig"];
};
ConfigView: {
config: components["schemas"]["RuntimeConfig"];
restart_required_fields: string[];
};
GatewayDto: {
admin_key_label: string;
id: string;
serial_number: string;
};
GatewayListResponse: {
items: components["schemas"]["GatewayDto"][];
};
HsmConfig: {
module_path: string;
pin_env_var: string;
/** Format: int64 */
slot?: number | null;
};
IconfigPreview: {
xml: string;
};
IconfigRequest: {
admin_key_label: string;
extras?: unknown;
gateway_id: string;
profile: string;
};
LoginRequest: {
/** @description Optional fallback subject for dev mode when no mTLS header is present. */
dev_subject?: string | null;
};
PauseRequest: {
paused: boolean;
};
RenewAccepted: {
message_id: string;
};
/**
* @description Mutable runtime config. Seeded from env on boot; UI may override at runtime
* for fields flagged `hot_reload`. Restart-only fields (BIND_ADDR) are read
* but cannot be applied without restart.
*/
RuntimeConfig: {
bind_addr: string;
cron_schedule: string;
database_url: string;
/** Format: int32 */
days_window: number;
hsm: components["schemas"]["HsmConfig"];
smtp: components["schemas"]["SmtpConfig"];
sub_ca: components["schemas"]["SubCaConfig"];
};
SchedulerState: {
cron_schedule: string;
/** Format: int32 */
days_window: number;
last_error?: string | null;
last_handled?: number | null;
/** Format: date-time */
last_run_at?: string | null;
last_run_ok?: boolean | null;
paused: boolean;
};
SessionResponse: {
/** Format: date-time */
expires_at: string;
subject: string;
};
SmtpConfig: {
from: string;
host: string;
/** Format: int32 */
port: number;
starttls: boolean;
to: string;
};
SubCaConfig: {
ca_bundle_path?: string | null;
client_cert_path?: string | null;
client_key_path?: string | null;
endpoint: string;
};
TestAlertRequest: {
body?: string | null;
subject?: string | null;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
list_alerts: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Recent alerts */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AlertListResponse"];
};
};
};
};
send_test_alert: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["TestAlertRequest"];
};
};
responses: {
/** @description Sent (or stub-logged) */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
whoami: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Current session */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SessionResponse"];
};
};
/** @description No active session */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiError"];
};
};
};
};
create_session: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["LoginRequest"];
};
};
responses: {
/** @description Session issued */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SessionResponse"];
};
};
/** @description No client cert subject */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiError"];
};
};
};
};
end_session: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Session ended */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
list_certificates: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Certificates */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CertListResponse"];
};
};
};
};
renew_certificate: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Gateway identifier */
gateway_id: string;
/** @description Certificate usage */
usage: components["schemas"]["CertificateUsageDto"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Renewal accepted */
202: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["RenewAccepted"];
};
};
/** @description Sub-CA adapter not implemented yet */
501: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiError"];
};
};
};
};
get_config: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Current runtime config */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ConfigView"];
};
};
};
};
update_config: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ConfigUpdate"];
};
};
responses: {
/** @description Updated runtime config */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ConfigView"];
};
};
};
};
list_gateways: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Gateways */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["GatewayListResponse"];
};
};
};
};
build_iconfig: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["IconfigRequest"];
};
};
responses: {
/** @description iconfig.tar */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/x-tar": unknown;
};
};
/** @description HSM signature not implemented */
501: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiError"];
};
};
};
};
preview_iconfig: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["IconfigRequest"];
};
};
responses: {
/** @description Preview XML */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["IconfigPreview"];
};
};
};
};
get_status: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Scheduler state */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SchedulerState"];
};
};
};
};
set_paused: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["PauseRequest"];
};
};
responses: {
/** @description Pause state updated */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SchedulerState"];
};
};
};
};
trigger_run: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Run accepted */
202: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Run already in progress */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiError"];
};
};
};
};
}
@@ -0,0 +1,94 @@
import { Link, useRouterState } from "@tanstack/react-router";
import {
LayoutGrid,
ShieldCheck,
Sliders,
FileLock2,
CircleDot,
} from "lucide-react";
import { cn } from "@/lib/utils";
type NavItem = {
to: string;
label: string;
hint: string;
icon: React.ComponentType<{ className?: string }>;
};
const NAV: NavItem[] = [
{ to: "/", label: "Übersicht", hint: "Status der PKI", icon: LayoutGrid },
{ to: "/certificates", label: "Zertifikate", hint: "Bestand & Erneuerung", icon: ShieldCheck },
{ to: "/configuration", label: "Konfiguration", hint: "Runtime-Parameter", icon: Sliders },
{ to: "/iconfig", label: "iconfig.tar", hint: "TR-03109-1 Builder", icon: FileLock2 },
];
export function Sidebar() {
const path = useRouterState({ select: (s) => s.location.pathname });
return (
<aside className="hidden lg:flex w-[260px] shrink-0 flex-col border-r border-line bg-paper/60 backdrop-blur-[2px]">
<div className="px-5 pt-6 pb-5 border-b border-line">
<Link to="/" className="flex items-center gap-3 group focus-ring rounded-md">
<div className="size-9 rounded-[10px] bg-ink text-paper grid place-items-center font-display font-semibold text-[15px] tracking-[-0.02em]">
sm
</div>
<div className="flex flex-col leading-tight">
<span className="font-display text-[14.5px] font-semibold tracking-[-0.015em] text-ink">
SMGW PKI
</span>
<span className="text-[11px] uppercase tracking-[0.08em] text-ink-faint">
Lab Console
</span>
</div>
</Link>
</div>
<nav className="flex-1 px-3 py-4 space-y-0.5">
<span className="block px-3 pb-2 text-[10.5px] uppercase tracking-[0.1em] text-ink-faint">
Steuerung
</span>
{NAV.map((item) => {
const active =
item.to === "/" ? path === "/" : path.startsWith(item.to);
const Icon = item.icon;
return (
<Link
key={item.to}
to={item.to}
className={cn(
"group flex items-start gap-3 rounded-[10px] px-3 py-2.5 text-[13px] transition-colors focus-ring",
active
? "bg-overlay text-ink"
: "text-ink-mute hover:bg-overlay/60 hover:text-ink",
)}
>
<Icon
className={cn(
"size-[18px] mt-0.5 shrink-0 transition-colors",
active ? "text-accent" : "text-ink-faint group-hover:text-ink-mute",
)}
/>
<span className="flex flex-col">
<span className="font-medium tracking-[-0.005em]">{item.label}</span>
<span className="text-[11.5px] text-ink-faint leading-tight">
{item.hint}
</span>
</span>
</Link>
);
})}
</nav>
<div className="m-3 surface-inset p-3.5">
<div className="flex items-center gap-2 text-[11px] uppercase tracking-[0.08em] text-ink-faint">
<CircleDot className="size-3 text-success" />
BSI-Kontext
</div>
<p className="mt-2 text-[12px] leading-relaxed text-ink-mute">
Test- und Laborbetrieb. SoftHSMv2, Sub-CA-Stub.{" "}
<span className="text-ink">Nicht für Produktion.</span>
</p>
</div>
</aside>
);
}
+84
View File
@@ -0,0 +1,84 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
import { LogOut, Play, ShieldHalf } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { api, unwrap } from "@/lib/api";
import { fmtRelative } from "@/lib/format";
export function Topbar({ title, subtitle }: { title: string; subtitle?: string }) {
const navigate = useNavigate();
const me = useQuery({
queryKey: ["auth", "me"],
queryFn: async () => {
try {
return await unwrap(api.GET("/auth/me"));
} catch {
return null;
}
},
});
const trigger = useMutation({
mutationFn: () => unwrap(api.POST("/scheduler/trigger")),
onSuccess: () => toast.success("Erneuerungslauf gestartet"),
onError: (e: Error) => toast.error(`Trigger fehlgeschlagen: ${e.message}`),
});
const logout = useMutation({
mutationFn: () => unwrap(api.DELETE("/auth/session")),
onSuccess: () => navigate({ to: "/login" }),
});
return (
<header className="sticky top-0 z-20 border-b border-line bg-canvas/85 backdrop-blur supports-[backdrop-filter]:bg-canvas/70">
<div className="flex items-center justify-between gap-6 px-6 lg:px-10 h-[68px]">
<div className="flex flex-col leading-tight">
<h1 className="font-display text-[20px] font-semibold tracking-[-0.02em] text-ink">
{title}
</h1>
{subtitle && (
<span className="text-[12.5px] text-ink-mute leading-tight">{subtitle}</span>
)}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => trigger.mutate()}
disabled={trigger.isPending}
>
<Play className="size-3.5" />
Lauf starten
</Button>
<div className="hidden md:flex items-center gap-2 pl-3 ml-1 border-l border-line">
<ShieldHalf className="size-4 text-accent" />
<div className="flex flex-col leading-tight">
<span className="text-[12px] font-medium text-ink mono">
{me.data?.subject ?? "—"}
</span>
<span className="text-[10.5px] uppercase tracking-[0.08em] text-ink-faint">
Sitzung läuft ab {fmtRelative(me.data?.expires_at)}
</span>
</div>
<Badge tone="accent" className="ml-2">mTLS</Badge>
</div>
<Button
variant="ghost"
size="icon"
aria-label="Abmelden"
onClick={() => logout.mutate()}
>
<LogOut className="size-4" />
</Button>
</div>
</div>
</header>
);
}
+25
View File
@@ -0,0 +1,25 @@
import { Badge } from "@/components/ui/badge";
export type CertState = "valid" | "expiring" | "expired" | "pending";
const STATE_TONE: Record<CertState, "success" | "warn" | "danger" | "neutral"> = {
valid: "success",
expiring: "warn",
expired: "danger",
pending: "neutral",
};
const STATE_LABEL: Record<CertState, string> = {
valid: "Gültig",
expiring: "Erneuerung fällig",
expired: "Abgelaufen",
pending: "Ausstehend",
};
export function StateBadge({ state }: { state: CertState }) {
return (
<Badge tone={STATE_TONE[state]} dot>
{STATE_LABEL[state]}
</Badge>
);
}
+32
View File
@@ -0,0 +1,32 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center gap-1.5 rounded-full px-2.5 h-[22px] text-[11px] font-medium tracking-[0.01em] uppercase border",
{
variants: {
tone: {
neutral: "bg-overlay text-ink-mute border-line",
accent: "bg-accent-soft text-accent border-accent-line",
success: "bg-success-soft text-success border-success/30",
warn: "bg-warn-soft text-warn border-warn/30",
danger: "bg-danger-soft text-danger border-danger/30",
outline: "bg-paper text-ink-mute border-line",
},
dot: {
true: "before:content-[''] before:w-1.5 before:h-1.5 before:rounded-full before:bg-current before:opacity-90",
false: "",
},
},
defaultVariants: { tone: "neutral", dot: false },
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof badgeVariants> {}
export function Badge({ className, tone, dot, ...props }: BadgeProps) {
return <span className={cn(badgeVariants({ tone, dot }), className)} {...props} />;
}
+54
View File
@@ -0,0 +1,54 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[8px] text-[13px] font-medium tracking-[-0.005em] transition-[background,box-shadow,transform] duration-100 disabled:pointer-events-none disabled:opacity-50 focus-ring active:scale-[0.985]",
{
variants: {
variant: {
primary:
"bg-ink text-paper hover:bg-ink/90 shadow-[inset_0_1px_0_rgba(255,255,255,0.08),0_1px_0_rgba(0,0,0,0.2)]",
accent:
"bg-accent text-paper hover:bg-accent/90 shadow-[inset_0_1px_0_rgba(255,255,255,0.16),0_1px_0_rgba(0,0,0,0.18)]",
outline:
"bg-paper text-ink border border-line hover:bg-overlay hover:border-line-strong",
ghost: "text-ink-mute hover:bg-overlay hover:text-ink",
soft: "bg-overlay text-ink hover:bg-line/60",
danger:
"bg-danger text-paper hover:bg-danger/90 shadow-[inset_0_1px_0_rgba(255,255,255,0.16)]",
link: "text-accent underline-offset-4 hover:underline px-0 h-auto",
},
size: {
sm: "h-7 px-2.5 text-[12px]",
md: "h-9 px-3.5",
lg: "h-10 px-5 text-[14px]",
icon: "h-9 w-9",
},
},
defaultVariants: { variant: "primary", size: "md" },
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { buttonVariants };
+44
View File
@@ -0,0 +1,44 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("surface", className)} {...props} />
),
);
Card.displayName = "Card";
export const CardHeader = ({ className, ...p }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex items-start justify-between gap-4 px-5 pt-5", className)} {...p} />
);
export const CardTitle = ({ className, ...p }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h3
className={cn(
"font-display text-[15px] font-semibold tracking-[-0.012em] text-ink",
className,
)}
{...p}
/>
);
export const CardDescription = ({
className,
...p
}: React.HTMLAttributes<HTMLParagraphElement>) => (
<p className={cn("text-[12.5px] text-ink-mute leading-relaxed", className)} {...p} />
);
export const CardBody = ({ className, ...p }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("px-5 py-5", className)} {...p} />
);
export const CardFooter = ({ className, ...p }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex items-center justify-between gap-3 border-t border-line bg-overlay/40 px-5 py-3 text-[12px] text-ink-mute rounded-b-[var(--radius-card)]",
className,
)}
{...p}
/>
);
+97
View File
@@ -0,0 +1,97 @@
import * as React from "react";
import * as RDialog from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
export const Dialog = RDialog.Root;
export const DialogTrigger = RDialog.Trigger;
export const DialogClose = RDialog.Close;
export const DialogPortal = ({ children, ...props }: RDialog.DialogPortalProps) => (
<RDialog.Portal {...props}>{children}</RDialog.Portal>
);
export const DialogOverlay = React.forwardRef<
React.ElementRef<typeof RDialog.Overlay>,
React.ComponentPropsWithoutRef<typeof RDialog.Overlay>
>(({ className, ...props }, ref) => (
<RDialog.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-ink/30 backdrop-blur-[2px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = "DialogOverlay";
export const DialogContent = React.forwardRef<
React.ElementRef<typeof RDialog.Content>,
React.ComponentPropsWithoutRef<typeof RDialog.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<RDialog.Content
ref={ref}
className={cn(
"fixed top-0 right-0 z-50 h-screen w-full max-w-[520px] bg-paper border-l border-line shadow-2xl outline-none flex flex-col",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right",
className,
)}
{...props}
>
{children}
<RDialog.Close className="absolute right-4 top-4 inline-flex h-7 w-7 items-center justify-center rounded-full text-ink-mute hover:bg-overlay hover:text-ink focus-ring">
<X className="size-4" />
<span className="sr-only">Schließen</span>
</RDialog.Close>
</RDialog.Content>
</DialogPortal>
));
DialogContent.displayName = "DialogContent";
export const DialogHeader = ({ className, ...p }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("border-b border-line px-6 py-5", className)} {...p} />
);
export const DialogTitle = React.forwardRef<
React.ElementRef<typeof RDialog.Title>,
React.ComponentPropsWithoutRef<typeof RDialog.Title>
>(({ className, ...props }, ref) => (
<RDialog.Title
ref={ref}
className={cn(
"font-display text-[18px] font-semibold tracking-[-0.015em] text-ink",
className,
)}
{...props}
/>
));
DialogTitle.displayName = "DialogTitle";
export const DialogDescription = React.forwardRef<
React.ElementRef<typeof RDialog.Description>,
React.ComponentPropsWithoutRef<typeof RDialog.Description>
>(({ className, ...props }, ref) => (
<RDialog.Description
ref={ref}
className={cn("text-[12.5px] text-ink-mute leading-relaxed mt-1", className)}
{...props}
/>
));
DialogDescription.displayName = "DialogDescription";
export const DialogBody = ({ className, ...p }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex-1 overflow-auto px-6 py-5", className)} {...p} />
);
export const DialogFooter = ({ className, ...p }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"border-t border-line bg-overlay/40 px-6 py-3 flex items-center justify-end gap-2",
className,
)}
{...p}
/>
);
+24
View File
@@ -0,0 +1,24 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
mono?: boolean;
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, mono, ...props }, ref) => (
<input
ref={ref}
className={cn(
"h-9 w-full rounded-[8px] border border-line bg-paper px-3 text-[13px] text-ink",
"placeholder:text-ink-faint focus-ring",
"transition-[border-color,background] duration-150",
"hover:border-line-strong focus:border-ink",
mono && "font-mono tracking-[-0.005em]",
className,
)}
{...props}
/>
),
);
Input.displayName = "Input";
+18
View File
@@ -0,0 +1,18 @@
import * as React from "react";
import * as RLabel from "@radix-ui/react-label";
import { cn } from "@/lib/utils";
export const Label = React.forwardRef<
React.ElementRef<typeof RLabel.Root>,
React.ComponentPropsWithoutRef<typeof RLabel.Root>
>(({ className, ...props }, ref) => (
<RLabel.Root
ref={ref}
className={cn(
"text-[11.5px] font-medium uppercase tracking-[0.06em] text-ink-mute",
className,
)}
{...props}
/>
));
Label.displayName = "Label";
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
import * as RSep from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
export const Separator = React.forwardRef<
React.ElementRef<typeof RSep.Root>,
React.ComponentPropsWithoutRef<typeof RSep.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<RSep.Root
ref={ref}
orientation={orientation}
decorative={decorative}
className={cn(
"shrink-0 bg-line",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className,
)}
{...props}
/>
));
Separator.displayName = "Separator";
+10
View File
@@ -0,0 +1,10 @@
import { cn } from "@/lib/utils";
export function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-[6px] bg-line/70", className)}
{...props}
/>
);
}
+27
View File
@@ -0,0 +1,27 @@
import * as React from "react";
import * as RSwitch from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
export const Switch = React.forwardRef<
React.ElementRef<typeof RSwitch.Root>,
React.ComponentPropsWithoutRef<typeof RSwitch.Root>
>(({ className, ...props }, ref) => (
<RSwitch.Root
ref={ref}
className={cn(
"peer inline-flex h-[20px] w-[34px] shrink-0 cursor-pointer items-center rounded-full border border-line bg-overlay transition-colors focus-ring",
"data-[state=checked]:bg-ink data-[state=checked]:border-ink",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RSwitch.Thumb
className={cn(
"pointer-events-none block h-[14px] w-[14px] rounded-full bg-paper shadow ring-0 transition-transform",
"data-[state=checked]:translate-x-[16px] data-[state=unchecked]:translate-x-[2px]",
)}
/>
</RSwitch.Root>
));
Switch.displayName = "Switch";
+64
View File
@@ -0,0 +1,64 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export function Table({ className, ...p }: React.HTMLAttributes<HTMLTableElement>) {
return (
<div className="w-full overflow-x-auto">
<table className={cn("w-full caption-bottom border-collapse text-[13px]", className)} {...p} />
</div>
);
}
export function THead({ className, ...p }: React.HTMLAttributes<HTMLTableSectionElement>) {
return (
<thead
className={cn(
"[&_tr]:border-b [&_tr]:border-line text-[11px] uppercase tracking-[0.06em] text-ink-faint",
className,
)}
{...p}
/>
);
}
export function TBody({ className, ...p }: React.HTMLAttributes<HTMLTableSectionElement>) {
return (
<tbody
className={cn(
"[&_tr]:border-b [&_tr]:border-line [&_tr:last-child]:border-0",
className,
)}
{...p}
/>
);
}
export function TR({ className, ...p }: React.HTMLAttributes<HTMLTableRowElement>) {
return (
<tr
className={cn("transition-colors hover:bg-overlay/60 data-[state=selected]:bg-overlay", className)}
{...p}
/>
);
}
export function TH({ className, ...p }: React.ThHTMLAttributes<HTMLTableCellElement>) {
return (
<th
className={cn(
"h-9 px-3 text-left align-middle font-medium first:pl-5 last:pr-5",
className,
)}
{...p}
/>
);
}
export function TD({ className, ...p }: React.TdHTMLAttributes<HTMLTableCellElement>) {
return (
<td
className={cn("py-2.5 px-3 align-middle first:pl-5 last:pr-5", className)}
{...p}
/>
);
}
+45
View File
@@ -0,0 +1,45 @@
import * as React from "react";
import * as RTabs from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
export const Tabs = RTabs.Root;
export const TabsList = React.forwardRef<
React.ElementRef<typeof RTabs.List>,
React.ComponentPropsWithoutRef<typeof RTabs.List>
>(({ className, ...props }, ref) => (
<RTabs.List
ref={ref}
className={cn(
"inline-flex items-center gap-1 p-1 rounded-[10px] bg-overlay border border-line",
className,
)}
{...props}
/>
));
TabsList.displayName = "TabsList";
export const TabsTrigger = React.forwardRef<
React.ElementRef<typeof RTabs.Trigger>,
React.ComponentPropsWithoutRef<typeof RTabs.Trigger>
>(({ className, ...props }, ref) => (
<RTabs.Trigger
ref={ref}
className={cn(
"px-3 h-7 inline-flex items-center text-[12.5px] font-medium tracking-[-0.005em] rounded-[7px] text-ink-mute transition-colors focus-ring",
"data-[state=active]:bg-paper data-[state=active]:text-ink data-[state=active]:shadow-sm",
"hover:text-ink",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = "TabsTrigger";
export const TabsContent = React.forwardRef<
React.ElementRef<typeof RTabs.Content>,
React.ComponentPropsWithoutRef<typeof RTabs.Content>
>(({ className, ...props }, ref) => (
<RTabs.Content ref={ref} className={cn("mt-5 focus-ring", className)} {...props} />
));
TabsContent.displayName = "TabsContent";
+11
View File
@@ -0,0 +1,11 @@
/// <reference types="vite/client" />
declare module "*.css" {
const content: string;
export default content;
}
declare module "*.svg" {
const content: string;
export default content;
}
+85
View File
@@ -0,0 +1,85 @@
@import "tailwindcss";
@theme {
/* Refined-minimal light theme. Warm paper canvas, ink primary, BSI-ish blue accent. */
--color-canvas: oklch(98.4% 0.005 95);
--color-paper: oklch(100% 0 0);
--color-ink: oklch(18% 0.01 270);
--color-ink-mute: oklch(42% 0.01 270);
--color-ink-faint: oklch(62% 0.01 270);
--color-line: oklch(91% 0.005 270);
--color-line-strong: oklch(82% 0.008 270);
--color-overlay: oklch(96% 0.005 270);
--color-accent: oklch(45% 0.16 260);
--color-accent-soft: oklch(96% 0.03 260);
--color-accent-line: oklch(86% 0.06 260);
--color-success: oklch(48% 0.12 152);
--color-success-soft: oklch(96% 0.04 152);
--color-warn: oklch(58% 0.16 65);
--color-warn-soft: oklch(96% 0.06 80);
--color-danger: oklch(50% 0.21 25);
--color-danger-soft: oklch(96% 0.04 25);
--font-display: "Bricolage Grotesque", system-ui, sans-serif;
--font-sans: "Geist", "Bricolage Grotesque", system-ui, sans-serif;
--font-mono: "Geist Mono", "JetBrains Mono", ui-monospace, monospace;
--font-serif: "Instrument Serif", "Iowan Old Style", Georgia, serif;
--radius-card: 12px;
--radius-control: 8px;
}
@layer base {
html, body, #root { height: 100%; }
body {
font-family: var(--font-sans);
font-feature-settings: "ss01", "cv11";
background:
radial-gradient(1200px 600px at 100% -10%, oklch(95% 0.025 260 / 0.6), transparent 60%),
radial-gradient(900px 500px at -10% 110%, oklch(95% 0.02 80 / 0.5), transparent 60%),
var(--color-canvas);
background-attachment: fixed;
}
::selection { background: var(--color-accent); color: white; }
h1, h2, h3, h4 { font-family: var(--font-display); letter-spacing: -0.02em; }
.num { font-feature-settings: "tnum", "ss02"; }
.mono { font-family: var(--font-mono); }
.serif { font-family: var(--font-serif); }
}
@layer components {
.surface {
background: var(--color-paper);
border: 1px solid var(--color-line);
border-radius: var(--radius-card);
box-shadow:
0 1px 0 rgba(20, 24, 40, 0.02),
0 8px 24px -20px rgba(20, 24, 40, 0.18);
}
.surface-inset {
background: var(--color-overlay);
border: 1px solid var(--color-line);
border-radius: var(--radius-card);
}
.hairline { border-color: var(--color-line); }
.focus-ring {
outline: 2px solid transparent;
outline-offset: 2px;
transition: box-shadow 120ms ease;
}
.focus-ring:focus-visible {
box-shadow:
0 0 0 2px var(--color-paper),
0 0 0 4px var(--color-accent);
}
.grid-bg {
background-image:
linear-gradient(var(--color-line) 1px, transparent 1px),
linear-gradient(90deg, var(--color-line) 1px, transparent 1px);
background-size: 28px 28px;
background-position: -1px -1px;
mask-image: radial-gradient(ellipse at 50% 0%, black 30%, transparent 75%);
}
}
+45
View File
@@ -0,0 +1,45 @@
import createClient from "openapi-fetch";
import type { paths, components } from "@/api/schema";
/**
* Single API client. `credentials: "include"` so the session cookie issued
* by `POST /api/auth/session` is sent with every subsequent request.
*
* In dev Vite proxies `/api/*` to the Rust backend (see vite.config.ts).
* In prod the SPA is served behind the same reverse proxy that terminates
* mTLS for the backend.
*/
export const api = createClient<paths>({
baseUrl: "/api",
credentials: "include",
});
export type Schemas = components["schemas"];
export class ApiError extends Error {
constructor(
public readonly status: number,
public readonly code: string,
message: string,
) {
super(message);
}
}
type FetchResult<T> = {
data?: T;
error?: { code?: string; message?: string };
response: Response;
};
export async function unwrap<T>(call: Promise<FetchResult<T>>): Promise<T> {
const { data, error, response } = await call;
if (!response.ok || error) {
throw new ApiError(
response.status,
error?.code ?? "unknown",
error?.message ?? response.statusText,
);
}
return data as T;
}
+24
View File
@@ -0,0 +1,24 @@
import { formatDistanceToNowStrict, formatISO9075, parseISO } from "date-fns";
export function fmtIso(input?: string | null): string {
if (!input) return "—";
try {
return formatISO9075(parseISO(input));
} catch {
return input;
}
}
export function fmtRelative(input?: string | null): string {
if (!input) return "—";
try {
return formatDistanceToNowStrict(parseISO(input), { addSuffix: true });
} catch {
return input;
}
}
export function truncateSerial(serial: string, head = 6, tail = 6): string {
if (serial.length <= head + tail + 1) return serial;
return `${serial.slice(0, head)}${serial.slice(-tail)}`;
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+49
View File
@@ -0,0 +1,49 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { Toaster } from "sonner";
import { routeTree } from "@/routeTree.gen";
import "@/index.css";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
retry: 1,
refetchOnWindowFocus: false,
},
},
});
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: "intent",
defaultPreloadStaleTime: 0,
});
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
const rootEl = document.getElementById("root")!;
createRoot(rootEl).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<Toaster
position="bottom-right"
toastOptions={{
classNames: {
toast:
"!bg-paper !text-ink !border !border-line !rounded-[10px] !font-sans !shadow-lg",
},
}}
/>
</QueryClientProvider>
</StrictMode>,
);
+161
View File
@@ -0,0 +1,161 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login'
import { Route as AppRouteImport } from './routes/_app'
import { Route as AppIndexRouteImport } from './routes/_app.index'
import { Route as AppIconfigRouteImport } from './routes/_app.iconfig'
import { Route as AppConfigurationRouteImport } from './routes/_app.configuration'
import { Route as AppCertificatesRouteImport } from './routes/_app.certificates'
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const AppRoute = AppRouteImport.update({
id: '/_app',
getParentRoute: () => rootRouteImport,
} as any)
const AppIndexRoute = AppIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AppRoute,
} as any)
const AppIconfigRoute = AppIconfigRouteImport.update({
id: '/iconfig',
path: '/iconfig',
getParentRoute: () => AppRoute,
} as any)
const AppConfigurationRoute = AppConfigurationRouteImport.update({
id: '/configuration',
path: '/configuration',
getParentRoute: () => AppRoute,
} as any)
const AppCertificatesRoute = AppCertificatesRouteImport.update({
id: '/certificates',
path: '/certificates',
getParentRoute: () => AppRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof AppIndexRoute
'/login': typeof LoginRoute
'/certificates': typeof AppCertificatesRoute
'/configuration': typeof AppConfigurationRoute
'/iconfig': typeof AppIconfigRoute
}
export interface FileRoutesByTo {
'/login': typeof LoginRoute
'/certificates': typeof AppCertificatesRoute
'/configuration': typeof AppConfigurationRoute
'/iconfig': typeof AppIconfigRoute
'/': typeof AppIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/_app': typeof AppRouteWithChildren
'/login': typeof LoginRoute
'/_app/certificates': typeof AppCertificatesRoute
'/_app/configuration': typeof AppConfigurationRoute
'/_app/iconfig': typeof AppIconfigRoute
'/_app/': typeof AppIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/login' | '/certificates' | '/configuration' | '/iconfig'
fileRoutesByTo: FileRoutesByTo
to: '/login' | '/certificates' | '/configuration' | '/iconfig' | '/'
id:
| '__root__'
| '/_app'
| '/login'
| '/_app/certificates'
| '/_app/configuration'
| '/_app/iconfig'
| '/_app/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
AppRoute: typeof AppRouteWithChildren
LoginRoute: typeof LoginRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/_app': {
id: '/_app'
path: ''
fullPath: '/'
preLoaderRoute: typeof AppRouteImport
parentRoute: typeof rootRouteImport
}
'/_app/': {
id: '/_app/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof AppIndexRouteImport
parentRoute: typeof AppRoute
}
'/_app/iconfig': {
id: '/_app/iconfig'
path: '/iconfig'
fullPath: '/iconfig'
preLoaderRoute: typeof AppIconfigRouteImport
parentRoute: typeof AppRoute
}
'/_app/configuration': {
id: '/_app/configuration'
path: '/configuration'
fullPath: '/configuration'
preLoaderRoute: typeof AppConfigurationRouteImport
parentRoute: typeof AppRoute
}
'/_app/certificates': {
id: '/_app/certificates'
path: '/certificates'
fullPath: '/certificates'
preLoaderRoute: typeof AppCertificatesRouteImport
parentRoute: typeof AppRoute
}
}
}
interface AppRouteChildren {
AppCertificatesRoute: typeof AppCertificatesRoute
AppConfigurationRoute: typeof AppConfigurationRoute
AppIconfigRoute: typeof AppIconfigRoute
AppIndexRoute: typeof AppIndexRoute
}
const AppRouteChildren: AppRouteChildren = {
AppCertificatesRoute: AppCertificatesRoute,
AppConfigurationRoute: AppConfigurationRoute,
AppIconfigRoute: AppIconfigRoute,
AppIndexRoute: AppIndexRoute,
}
const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren)
const rootRouteChildren: RootRouteChildren = {
AppRoute: AppRouteWithChildren,
LoginRoute: LoginRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
+10
View File
@@ -0,0 +1,10 @@
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import type { QueryClient } from "@tanstack/react-query";
export interface RouterContext {
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => <Outlet />,
});
+456
View File
@@ -0,0 +1,456 @@
import { useMemo, useState } from "react";
import {
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { toast } from "sonner";
import {
Copy,
RefreshCcw,
Search,
ShieldCheck,
ArrowUpDown,
Eye,
} from "lucide-react";
import { Topbar } from "@/components/layout/topbar";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Table,
THead,
TBody,
TR,
TH,
TD,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { StateBadge, type CertState } from "@/components/state-badge";
import { Skeleton } from "@/components/ui/skeleton";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogBody,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { api, unwrap, type Schemas, ApiError } from "@/lib/api";
import { fmtIso, fmtRelative, truncateSerial } from "@/lib/format";
import { cn } from "@/lib/utils";
export const Route = createFileRoute("/_app/certificates")({
component: CertificatesPage,
});
type SortKey = "gateway" | "usage" | "not_after" | "state";
function CertificatesPage() {
const [q, setQ] = useState("");
const [stateFilter, setStateFilter] = useState<"all" | CertState>("all");
const [sortKey, setSortKey] = useState<SortKey>("not_after");
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
const [selected, setSelected] = useState<Schemas["CertificateDto"] | null>(null);
const qc = useQueryClient();
const certs = useQuery({
queryKey: ["certs"],
queryFn: () => unwrap(api.GET("/certs")),
refetchInterval: 30_000,
});
const renew = useMutation({
mutationFn: (input: { gateway_id: string; usage: Schemas["CertificateUsageDto"] }) =>
unwrap(
api.POST("/certs/{gateway_id}/{usage}/renew", {
params: { path: input },
}),
),
onSuccess: (data) => {
toast.success("Erneuerung beauftragt", {
description: `messageID ${data.message_id}`,
});
qc.invalidateQueries({ queryKey: ["certs"] });
},
onError: (e: ApiError) =>
toast.error("Erneuerung abgelehnt", { description: e.message }),
});
const filtered = useMemo(() => {
const items = certs.data?.items ?? [];
return items
.filter((c) => (stateFilter === "all" ? true : c.state === stateFilter))
.filter((c) =>
q.trim().length === 0
? true
: `${c.gateway_id} ${c.serial} ${c.usage}`
.toLowerCase()
.includes(q.toLowerCase()),
)
.sort((a, b) => {
const dir = sortDir === "asc" ? 1 : -1;
const cmp = compareBy(sortKey, a, b);
return cmp * dir;
});
}, [certs.data, q, stateFilter, sortKey, sortDir]);
return (
<>
<Topbar
title="Zertifikate"
subtitle="Bestand pro Gateway-Profil. Sortier-, Filter- und Erneuerungsaktionen."
/>
<div className="px-6 lg:px-10 py-7 space-y-5 max-w-[1320px]">
<div className="flex flex-wrap items-center gap-3">
<div className="relative w-full sm:w-[320px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-3.5 text-ink-faint" />
<Input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Gateway, Serial, Profil…"
className="pl-9"
/>
</div>
<div className="flex items-center gap-1 surface p-1">
{(["all", "valid", "expiring", "expired"] as const).map((s) => (
<button
key={s}
onClick={() => setStateFilter(s)}
className={cn(
"px-2.5 h-7 inline-flex items-center text-[11.5px] uppercase tracking-[0.06em] rounded-[6px] transition-colors",
stateFilter === s
? "bg-ink text-paper"
: "text-ink-mute hover:text-ink hover:bg-overlay",
)}
>
{LABEL_STATE[s]}
</button>
))}
</div>
<div className="ml-auto flex items-center gap-2 text-[12px] text-ink-mute">
<Badge tone="outline">
{filtered.length}/{certs.data?.items.length ?? 0}
</Badge>
Einträge
</div>
</div>
<Card className="overflow-hidden p-0">
<Table>
<THead>
<TR>
<ColHeader
label="Gateway / Profil"
active={sortKey === "gateway"}
dir={sortDir}
onClick={() => toggleSort("gateway", sortKey, sortDir, setSortKey, setSortDir)}
/>
<TH>Serial</TH>
<ColHeader
label="Verwendung"
active={sortKey === "usage"}
dir={sortDir}
onClick={() => toggleSort("usage", sortKey, sortDir, setSortKey, setSortDir)}
/>
<ColHeader
label="Gültig bis"
active={sortKey === "not_after"}
dir={sortDir}
onClick={() => toggleSort("not_after", sortKey, sortDir, setSortKey, setSortDir)}
/>
<TH>Restlaufzeit</TH>
<ColHeader
label="Status"
active={sortKey === "state"}
dir={sortDir}
onClick={() => toggleSort("state", sortKey, sortDir, setSortKey, setSortDir)}
/>
<TH className="text-right">Aktionen</TH>
</TR>
</THead>
<TBody>
{certs.isLoading && <TableSkeleton />}
{!certs.isLoading && filtered.length === 0 && (
<TR>
<TD colSpan={7} className="py-14 text-center text-ink-mute">
Keine Treffer.
</TD>
</TR>
)}
{filtered.map((c) => (
<TR key={`${c.gateway_id}-${c.usage}`}>
<TD>
<div className="flex flex-col leading-tight">
<span className="text-[13px] font-medium text-ink">
{c.gateway_id}
</span>
<span className="text-[11px] text-ink-faint">
Profil · {LABEL_USAGE[c.usage]}
</span>
</div>
</TD>
<TD>
<button
onClick={() => copySerial(c.serial)}
className="group inline-flex items-center gap-1.5 mono text-[12px] text-ink-mute hover:text-ink"
title={c.serial}
>
{truncateSerial(c.serial)}
<Copy className="size-3 opacity-0 group-hover:opacity-100 transition-opacity" />
</button>
</TD>
<TD>
<Badge tone="outline">{LABEL_USAGE[c.usage]}</Badge>
</TD>
<TD className="num text-[12.5px] text-ink">{fmtIso(c.not_after)}</TD>
<TD>
<span
className={cn(
"num text-[12.5px]",
c.days_to_expiry < 0
? "text-danger"
: c.days_to_expiry <= 30
? "text-warn"
: "text-ink-mute",
)}
>
{fmtRelative(c.not_after)}
</span>
</TD>
<TD>
<StateBadge state={c.state as CertState} />
</TD>
<TD className="text-right">
<div className="inline-flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => setSelected(c)}
>
<Eye className="size-3.5" />
Details
</Button>
<Button
variant="outline"
size="sm"
disabled={renew.isPending}
onClick={() =>
renew.mutate({ gateway_id: c.gateway_id, usage: c.usage })
}
>
<RefreshCcw className="size-3.5" />
Erneuern
</Button>
</div>
</TD>
</TR>
))}
</TBody>
</Table>
</Card>
</div>
<Dialog
open={selected !== null}
onOpenChange={(o) => !o && setSelected(null)}
>
{selected && <DetailDrawer cert={selected} />}
</Dialog>
</>
);
}
function DetailDrawer({ cert }: { cert: Schemas["CertificateDto"] }) {
return (
<DialogContent>
<DialogHeader>
<div className="flex items-center gap-2 text-[11px] uppercase tracking-[0.08em] text-ink-faint">
<ShieldCheck className="size-3.5 text-accent" />
Zertifikatsdetails
</div>
<DialogTitle className="mt-1.5">{cert.gateway_id}</DialogTitle>
<DialogDescription>
Verwendung {LABEL_USAGE[cert.usage]} · Status{" "}
<span className="text-ink">{LABEL_STATE[cert.state as CertState]}</span>
</DialogDescription>
</DialogHeader>
<DialogBody>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Übersicht</TabsTrigger>
<TabsTrigger value="raw">PEM</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-5">
<Field label="Serial">
<code className="mono break-all text-[12px] text-ink">{cert.serial}</code>
</Field>
<div className="grid grid-cols-2 gap-5">
<Field label="Gültig ab">{fmtIso(cert.not_before)}</Field>
<Field label="Gültig bis">{fmtIso(cert.not_after)}</Field>
</div>
<Field label="Restlaufzeit">
<span
className={cn(
"num",
cert.days_to_expiry < 0
? "text-danger"
: cert.days_to_expiry <= 30
? "text-warn"
: "text-ink",
)}
>
{cert.days_to_expiry} Tage
</span>
</Field>
</TabsContent>
<TabsContent value="raw">
<pre className="surface-inset p-4 text-[11.5px] mono whitespace-pre-wrap break-all text-ink-mute">
{`-----BEGIN CERTIFICATE-----
[Backend liefert PEM nach Anbindung an /api/certs/{id}/pem]
-----END CERTIFICATE-----`}
</pre>
</TabsContent>
</Tabs>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" size="sm">Schließen</Button>
</DialogClose>
<Button variant="primary" size="sm">
<RefreshCcw className="size-3.5" />
Erneuern
</Button>
</DialogFooter>
</DialogContent>
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="space-y-1">
<span className="text-[10.5px] uppercase tracking-[0.08em] text-ink-faint">
{label}
</span>
<div className="text-[13px] text-ink">{children}</div>
</div>
);
}
function ColHeader({
label,
active,
dir,
onClick,
}: {
label: string;
active: boolean;
dir: "asc" | "desc";
onClick: () => void;
}) {
return (
<TH>
<button
onClick={onClick}
className={cn(
"inline-flex items-center gap-1 text-[11px] uppercase tracking-[0.06em]",
active ? "text-ink" : "text-ink-faint hover:text-ink",
)}
>
{label}
<ArrowUpDown
className={cn(
"size-3 transition-transform",
active && dir === "desc" && "rotate-180",
!active && "opacity-40",
)}
/>
</button>
</TH>
);
}
function TableSkeleton() {
return (
<>
{[0, 1, 2, 3].map((i) => (
<TR key={i}>
{Array.from({ length: 7 }).map((_, j) => (
<TD key={j}>
<Skeleton className="h-4 w-full max-w-[180px]" />
</TD>
))}
</TR>
))}
</>
);
}
const LABEL_USAGE: Record<Schemas["CertificateUsageDto"], string> = {
tls: "TLS",
signature: "Signatur",
encryption: "Verschlüsselung",
};
const LABEL_STATE: Record<"all" | CertState, string> = {
all: "Alle",
valid: "Gültig",
expiring: "Erneuerung",
expired: "Abgelaufen",
pending: "Ausstehend",
};
function compareBy(
key: SortKey,
a: Schemas["CertificateDto"],
b: Schemas["CertificateDto"],
): number {
switch (key) {
case "gateway":
return a.gateway_id.localeCompare(b.gateway_id);
case "usage":
return a.usage.localeCompare(b.usage);
case "not_after":
return a.not_after.localeCompare(b.not_after);
case "state":
return STATE_ORDER[a.state] - STATE_ORDER[b.state];
}
}
const STATE_ORDER: Record<string, number> = {
expired: 0,
expiring: 1,
valid: 2,
};
function toggleSort(
next: SortKey,
current: SortKey,
dir: "asc" | "desc",
setKey: (k: SortKey) => void,
setDir: (d: "asc" | "desc") => void,
) {
if (current === next) {
setDir(dir === "asc" ? "desc" : "asc");
} else {
setKey(next);
setDir("asc");
}
}
async function copySerial(serial: string) {
try {
await navigator.clipboard.writeText(serial);
toast.success("Serial kopiert");
} catch {
toast.error("Kopieren fehlgeschlagen");
}
}
+457
View File
@@ -0,0 +1,457 @@
import { useEffect, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { toast } from "sonner";
import {
Cable,
Cpu,
Mailbox,
RotateCcw,
Save,
Settings2,
TimerReset,
} from "lucide-react";
import { Topbar } from "@/components/layout/topbar";
import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator";
import { api, unwrap, type Schemas, ApiError } from "@/lib/api";
import { cn } from "@/lib/utils";
export const Route = createFileRoute("/_app/configuration")({
component: ConfigurationPage,
});
function ConfigurationPage() {
const qc = useQueryClient();
const view = useQuery({
queryKey: ["config"],
queryFn: () => unwrap(api.GET("/config")),
});
const [draft, setDraft] = useState<Schemas["RuntimeConfig"] | null>(null);
useEffect(() => {
if (view.data && !draft) setDraft(view.data.config);
}, [view.data, draft]);
const save = useMutation({
mutationFn: (body: Schemas["ConfigUpdate"]) =>
unwrap(api.PUT("/config", { body })),
onSuccess: (data) => {
toast.success("Konfiguration gespeichert");
qc.setQueryData(["config"], data);
setDraft(data.config);
},
onError: (e: ApiError) =>
toast.error("Speichern fehlgeschlagen", { description: e.message }),
});
const testAlert = useMutation({
mutationFn: () =>
unwrap(
api.POST("/alerts/test", {
body: { subject: "SMTP-Test", body: "Konsole → Mail-Adapter" },
}),
),
onSuccess: () => toast.success("Test-Alert gesendet"),
onError: (e: ApiError) => toast.error("Test fehlgeschlagen", { description: e.message }),
});
if (!draft || !view.data) {
return (
<>
<Topbar title="Konfiguration" subtitle="Lade…" />
<div className="px-6 lg:px-10 py-7">
<div className="surface h-[60vh] grid place-items-center text-ink-faint">
Bereite Formular vor.
</div>
</div>
</>
);
}
const dirty = JSON.stringify(draft) !== JSON.stringify(view.data.config);
const restartFields = new Set(view.data.restart_required_fields);
const patch: Schemas["ConfigUpdate"] = {
cron_schedule: draft.cron_schedule,
days_window: draft.days_window,
sub_ca: draft.sub_ca,
smtp: draft.smtp,
hsm: draft.hsm,
};
return (
<>
<Topbar
title="Konfiguration"
subtitle="Runtime-Parameter und externe Anbindungen. Restart-pflichtige Felder sind markiert."
/>
<div className="px-6 lg:px-10 py-7 pb-32 max-w-[1100px] space-y-6">
<SectionCard
icon={TimerReset}
title="Scheduler"
desc="Cron-Ausdruck und Erneuerungsfenster. Wird zur Laufzeit übernommen."
>
<Row>
<Field label="Cron-Ausdruck (6-Field)">
<Input
mono
value={draft.cron_schedule}
onChange={(e) =>
setDraft({ ...draft, cron_schedule: e.target.value })
}
/>
<Hint>Sekunde Minute Stunde Tag Monat Wochentag.</Hint>
</Field>
<Field label="Tagesfenster">
<Input
type="number"
min={1}
max={365}
value={draft.days_window}
onChange={(e) =>
setDraft({ ...draft, days_window: Number(e.target.value) })
}
/>
<Hint>Default 30. SM-PKI CP empfiehlt frühe Erneuerung.</Hint>
</Field>
</Row>
</SectionCard>
<SectionCard
icon={Cable}
title="Sub-CA / SOAP"
desc="Endpunkt der Test-Sub-CA, mTLS-Material gem. TR-03129-4."
>
<Row>
<Field label="Endpoint">
<Input
mono
value={draft.sub_ca.endpoint}
onChange={(e) =>
setDraft({
...draft,
sub_ca: { ...draft.sub_ca, endpoint: e.target.value },
})
}
placeholder="https://test-ca.local/soap"
/>
</Field>
</Row>
<Row>
<Field label="Client-Cert (Pfad)">
<Input
mono
value={draft.sub_ca.client_cert_path ?? ""}
onChange={(e) =>
setDraft({
...draft,
sub_ca: { ...draft.sub_ca, client_cert_path: e.target.value },
})
}
placeholder="/etc/smgw/ca-client.crt.pem"
/>
</Field>
<Field label="Client-Key (Pfad)">
<Input
mono
value={draft.sub_ca.client_key_path ?? ""}
onChange={(e) =>
setDraft({
...draft,
sub_ca: { ...draft.sub_ca, client_key_path: e.target.value },
})
}
placeholder="/etc/smgw/ca-client.key.pem"
/>
</Field>
</Row>
<Row>
<Field label="CA-Bundle (Pfad)">
<Input
mono
value={draft.sub_ca.ca_bundle_path ?? ""}
onChange={(e) =>
setDraft({
...draft,
sub_ca: { ...draft.sub_ca, ca_bundle_path: e.target.value },
})
}
placeholder="/etc/smgw/sub-ca-chain.pem"
/>
</Field>
</Row>
</SectionCard>
<SectionCard
icon={Mailbox}
title="SMTP / Alerts"
desc="Operator-Benachrichtigungen bei Fehlern."
aside={
<Button
variant="outline"
size="sm"
onClick={() => testAlert.mutate()}
disabled={testAlert.isPending}
>
Test-Alert
</Button>
}
>
<Row>
<Field label="Host">
<Input
mono
value={draft.smtp.host}
onChange={(e) =>
setDraft({
...draft,
smtp: { ...draft.smtp, host: e.target.value },
})
}
/>
</Field>
<Field label="Port">
<Input
type="number"
value={draft.smtp.port}
onChange={(e) =>
setDraft({
...draft,
smtp: { ...draft.smtp, port: Number(e.target.value) },
})
}
/>
</Field>
<Field label="STARTTLS">
<div className="flex items-center gap-3 pt-1">
<Switch
checked={draft.smtp.starttls}
onCheckedChange={(v) =>
setDraft({
...draft,
smtp: { ...draft.smtp, starttls: v },
})
}
/>
<span className="text-[12.5px] text-ink-mute">
{draft.smtp.starttls ? "aktiv" : "inaktiv"}
</span>
</div>
</Field>
</Row>
<Row>
<Field label="Absender">
<Input
value={draft.smtp.from}
onChange={(e) =>
setDraft({
...draft,
smtp: { ...draft.smtp, from: e.target.value },
})
}
/>
</Field>
<Field label="Empfänger">
<Input
value={draft.smtp.to}
onChange={(e) =>
setDraft({
...draft,
smtp: { ...draft.smtp, to: e.target.value },
})
}
/>
</Field>
</Row>
</SectionCard>
<SectionCard
icon={Cpu}
title="HSM (PKCS#11)"
desc="SoftHSMv2-Modul, Slot und PIN-Quelle. Produktion: zertifiziertes HSM gem. SM-PKI CP."
>
<Row>
<Field label="Modul-Pfad">
<Input
mono
value={draft.hsm.module_path}
onChange={(e) =>
setDraft({
...draft,
hsm: { ...draft.hsm, module_path: e.target.value },
})
}
/>
</Field>
<Field label="Slot">
<Input
type="number"
value={draft.hsm.slot ?? ""}
onChange={(e) =>
setDraft({
...draft,
hsm: {
...draft.hsm,
slot: e.target.value === "" ? null : Number(e.target.value),
},
})
}
placeholder="auto"
/>
</Field>
<Field label="PIN-Env-Var">
<Input
mono
value={draft.hsm.pin_env_var}
onChange={(e) =>
setDraft({
...draft,
hsm: { ...draft.hsm, pin_env_var: e.target.value },
})
}
/>
</Field>
</Row>
</SectionCard>
<SectionCard
icon={Settings2}
title="Restart-pflichtig"
desc="Diese Felder werden nur beim Boot angewendet."
>
<Row>
<Field label="Bind-Adresse" restartRequired={restartFields.has("bind_addr")}>
<Input mono value={draft.bind_addr} readOnly disabled />
</Field>
<Field
label="Database-URL"
restartRequired={restartFields.has("database_url")}
>
<Input mono value={draft.database_url} readOnly disabled />
</Field>
</Row>
</SectionCard>
</div>
<SaveBar
visible={dirty}
onRevert={() => view.data && setDraft(view.data.config)}
onSave={() => save.mutate(patch)}
saving={save.isPending}
/>
</>
);
}
function SectionCard({
icon: Icon,
title,
desc,
aside,
children,
}: {
icon: React.ComponentType<{ className?: string }>;
title: string;
desc: string;
aside?: React.ReactNode;
children: React.ReactNode;
}) {
return (
<Card>
<CardHeader>
<div className="flex items-start gap-3">
<div className="size-8 rounded-[8px] bg-overlay grid place-items-center mt-0.5">
<Icon className="size-4 text-ink-mute" />
</div>
<div>
<CardTitle>{title}</CardTitle>
<p className="mt-1 text-[12.5px] text-ink-mute max-w-md leading-relaxed">
{desc}
</p>
</div>
</div>
{aside}
</CardHeader>
<Separator />
<CardBody className="space-y-5">{children}</CardBody>
</Card>
);
}
function Row({ children }: { children: React.ReactNode }) {
return (
<div className="grid gap-5 md:grid-cols-2 lg:[&>*:nth-child(3)]:col-span-1 lg:grid-cols-3">
{children}
</div>
);
}
function Field({
label,
restartRequired,
children,
}: {
label: string;
restartRequired?: boolean;
children: React.ReactNode;
}) {
return (
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-2">
<Label>{label}</Label>
{restartRequired && <Badge tone="warn">Restart nötig</Badge>}
</div>
{children}
</div>
);
}
function Hint({ children }: { children: React.ReactNode }) {
return <p className="text-[11.5px] text-ink-faint">{children}</p>;
}
function SaveBar({
visible,
onRevert,
onSave,
saving,
}: {
visible: boolean;
onRevert: () => void;
onSave: () => void;
saving: boolean;
}) {
return (
<div
aria-hidden={!visible}
className={cn(
"fixed bottom-5 left-1/2 -translate-x-1/2 z-30 transition-all duration-200",
visible
? "opacity-100 translate-y-0"
: "opacity-0 pointer-events-none translate-y-2",
)}
>
<div className="surface px-4 py-3 flex items-center gap-3 shadow-xl">
<div className="flex items-center gap-2 text-[12.5px]">
<span className="size-1.5 rounded-full bg-warn animate-pulse" />
<span className="text-ink">Ungespeicherte Änderungen</span>
</div>
<Button variant="ghost" size="sm" onClick={onRevert}>
<RotateCcw className="size-3.5" />
Verwerfen
</Button>
<Button variant="primary" size="sm" onClick={onSave} disabled={saving}>
<Save className="size-3.5" />
Speichern
</Button>
</div>
</div>
);
}
+230
View File
@@ -0,0 +1,230 @@
import { useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { toast } from "sonner";
import {
Download,
FileLock2,
KeyRound,
ScanLine,
Shield,
Wand2,
} from "lucide-react";
import { Topbar } from "@/components/layout/topbar";
import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { api, unwrap, type Schemas, ApiError } from "@/lib/api";
export const Route = createFileRoute("/_app/iconfig")({
component: IconfigPage,
});
const PROFILES = [
{ id: "smgw-default", label: "SMGW Default", note: "TR-03109-1 Standardprofil" },
{ id: "lab-debug", label: "Lab Debug", note: "Erweiterte Diagnose-Felder" },
];
function IconfigPage() {
const gateways = useQuery({
queryKey: ["gateways"],
queryFn: () => unwrap(api.GET("/gateways")),
});
const [form, setForm] = useState<Schemas["IconfigRequest"]>({
gateway_id: "",
admin_key_label: "",
profile: PROFILES[0].id,
});
const [preview, setPreview] = useState<string | null>(null);
const previewMut = useMutation({
mutationFn: (body: Schemas["IconfigRequest"]) =>
unwrap(api.POST("/iconfig/preview", { body })),
onSuccess: (d) => setPreview(d.xml),
onError: (e: ApiError) =>
toast.error("Vorschau fehlgeschlagen", { description: e.message }),
});
const buildMut = useMutation({
mutationFn: (body: Schemas["IconfigRequest"]) =>
unwrap(api.POST("/iconfig/build", { body })),
onError: (e: ApiError) =>
toast.error("Build abgelehnt", { description: e.message }),
});
const ready = form.gateway_id.length > 0 && form.admin_key_label.length > 0;
return (
<>
<Topbar
title="iconfig.tar"
subtitle="Initial-Konfiguration gemäß BSI TR-03109-1, signiert via HSM."
/>
<div className="px-6 lg:px-10 py-7 grid gap-6 max-w-[1320px] xl:grid-cols-[480px_1fr]">
<Card>
<CardHeader>
<div className="flex items-start gap-3">
<div className="size-8 rounded-[8px] bg-accent-soft grid place-items-center mt-0.5">
<FileLock2 className="size-4 text-accent" />
</div>
<div>
<CardTitle>Profil zusammensetzen</CardTitle>
<p className="mt-1 text-[12.5px] text-ink-mute leading-relaxed">
Auswahl Gateway, Admin-Schlüssel, Profilvorlage. Vorschau bevor signiert wird.
</p>
</div>
</div>
</CardHeader>
<Separator />
<CardBody className="space-y-5">
<div className="space-y-1.5">
<Label>Gateway</Label>
<select
value={form.gateway_id}
onChange={(e) => setForm({ ...form, gateway_id: e.target.value })}
className="h-9 w-full rounded-[8px] border border-line bg-paper px-3 text-[13px] text-ink focus-ring hover:border-line-strong focus:border-ink mono"
>
<option value=""> bitte wählen </option>
{(gateways.data?.items ?? []).map((g) => (
<option key={g.id} value={g.id}>
{g.id} · {g.serial_number}
</option>
))}
</select>
<p className="text-[11.5px] text-ink-faint">
{gateways.data?.items.length ?? 0} Gateways registriert.
</p>
</div>
<div className="space-y-1.5">
<Label>Admin-Key Label</Label>
<Input
mono
value={form.admin_key_label}
onChange={(e) =>
setForm({ ...form, admin_key_label: e.target.value })
}
placeholder="GW-ADM-01"
/>
<p className="text-[11.5px] text-ink-faint">
PKCS#11-Label im HSM. Wird über sign_xml referenziert.
</p>
</div>
<div className="space-y-2">
<Label>Profil</Label>
<div className="grid gap-2">
{PROFILES.map((p) => {
const selected = form.profile === p.id;
return (
<button
key={p.id}
type="button"
onClick={() => setForm({ ...form, profile: p.id })}
className={`text-left rounded-[10px] border p-3 transition-colors focus-ring ${
selected
? "border-ink bg-overlay"
: "border-line hover:border-line-strong hover:bg-overlay/60"
}`}
>
<div className="flex items-center justify-between">
<span className="text-[13px] font-medium text-ink">
{p.label}
</span>
{selected && <Badge tone="accent">Aktiv</Badge>}
</div>
<p className="mt-1 text-[12px] text-ink-mute">{p.note}</p>
</button>
);
})}
</div>
</div>
<Separator />
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="md"
disabled={!ready || previewMut.isPending}
onClick={() => previewMut.mutate(form)}
>
<ScanLine className="size-3.5" />
Vorschau erzeugen
</Button>
<Button
variant="primary"
size="md"
disabled={!ready || buildMut.isPending}
onClick={() => buildMut.mutate(form)}
>
<Wand2 className="size-3.5" />
Signieren & herunterladen
</Button>
</div>
<div className="surface-inset p-3 text-[11.5px] text-ink-mute leading-relaxed">
<div className="flex items-start gap-2">
<Shield className="size-3.5 text-ink-faint mt-0.5 shrink-0" />
<div>
Signatur erfolgt server-seitig über{" "}
<code className="mono text-[11px] text-ink">HsmPort::sign_xml</code>{" "}
auf kanonisierten Bytes (C14N), nicht auf pretty-printed XML.
</div>
</div>
</div>
</CardBody>
</Card>
<Card>
<CardHeader>
<div className="flex items-start gap-3">
<div className="size-8 rounded-[8px] bg-overlay grid place-items-center mt-0.5">
<KeyRound className="size-4 text-ink-mute" />
</div>
<div>
<CardTitle>Vorschau</CardTitle>
<p className="mt-1 text-[12.5px] text-ink-mute leading-relaxed">
Unsignierter XML-Body. Inhalt wird vor Signatur kanonisiert.
</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
disabled={!preview}
onClick={() => preview && downloadXml(preview, form.gateway_id)}
>
<Download className="size-3.5" />
Als XML speichern
</Button>
</CardHeader>
<Separator />
<CardBody className="p-0">
<pre className="m-0 max-h-[600px] overflow-auto p-5 text-[12px] mono whitespace-pre text-ink leading-relaxed bg-paper">
{preview ??
`// Vorschau erscheint hier, sobald „Vorschau erzeugen" geklickt wurde.
// Die Datei wird auf dem Backend gebaut — diese Konsole rendert nur das Ergebnis.`}
</pre>
</CardBody>
</Card>
</div>
</>
);
}
function downloadXml(xml: string, gateway: string) {
const blob = new Blob([xml], { type: "application/xml" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `iconfig-${gateway || "preview"}.xml`;
a.click();
URL.revokeObjectURL(url);
}
+381
View File
@@ -0,0 +1,381 @@
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import {
Activity,
AlertCircle,
CalendarClock,
CheckCircle2,
Clock,
Hourglass,
Info,
ShieldAlert,
TriangleAlert,
} from "lucide-react";
import { Topbar } from "@/components/layout/topbar";
import { Card, CardBody, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { api, unwrap, type Schemas } from "@/lib/api";
import { fmtIso, fmtRelative } from "@/lib/format";
import { cn } from "@/lib/utils";
export const Route = createFileRoute("/_app/")({
component: Dashboard,
});
function Dashboard() {
const certs = useQuery({
queryKey: ["certs"],
queryFn: () => unwrap(api.GET("/certs")),
refetchInterval: 60_000,
});
const scheduler = useQuery({
queryKey: ["scheduler"],
queryFn: () => unwrap(api.GET("/scheduler")),
refetchInterval: 10_000,
});
const alerts = useQuery({
queryKey: ["alerts"],
queryFn: () => unwrap(api.GET("/alerts")),
refetchInterval: 30_000,
});
const counts = countByState(certs.data?.items ?? []);
return (
<>
<Topbar
title="Übersicht"
subtitle="Bestand, Erneuerungsplan und letzte Ereignisse der Sub-CA-Anbindung."
/>
<div className="px-6 lg:px-10 py-7 space-y-7 max-w-[1320px]">
<section className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
<StatCard
label="Gültig"
value={counts.valid}
hint={`${counts.total} Zertifikate gesamt`}
tone="success"
icon={CheckCircle2}
/>
<StatCard
label="Erneuerung fällig"
value={counts.expiring}
hint={`im ${scheduler.data?.days_window ?? 30}-Tage-Fenster`}
tone="warn"
icon={Hourglass}
emphasise={counts.expiring > 0}
/>
<StatCard
label="Abgelaufen"
value={counts.expired}
hint="Manueller Eingriff nötig"
tone="danger"
icon={TriangleAlert}
/>
<StatCard
label="In Bearbeitung"
value={counts.pending}
hint="Auf CA-Callback wartend"
tone="neutral"
icon={Clock}
/>
</section>
<section className="grid gap-4 lg:grid-cols-[1.4fr_1fr]">
<Card>
<CardHeader>
<div>
<CardTitle>Renewal-Scheduler</CardTitle>
<p className="text-[12.5px] text-ink-mute mt-1">
Periodischer Lauf gem. SM-PKI CP. Manueller Trigger via Topbar.
</p>
</div>
<Badge
tone={scheduler.data?.paused ? "warn" : "success"}
dot
>
{scheduler.data?.paused ? "Pausiert" : "Aktiv"}
</Badge>
</CardHeader>
<CardBody className="grid grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-5">
<KeyValue
label="Cron-Ausdruck"
value={
<code className="mono text-[12.5px] text-ink">
{scheduler.data?.cron_schedule ?? "—"}
</code>
}
/>
<KeyValue
label="Fenster"
value={
<span className="num text-[14px] text-ink">
{scheduler.data?.days_window ?? "—"} Tage
</span>
}
/>
<KeyValue
label="Bearbeitet (zuletzt)"
value={
<span className="num text-[14px] text-ink">
{scheduler.data?.last_handled ?? 0}
</span>
}
/>
<KeyValue
label="Letzter Lauf"
value={
<div className="flex items-center gap-2">
<CalendarClock className="size-3.5 text-ink-faint" />
<span className="text-[13px] text-ink">
{fmtRelative(scheduler.data?.last_run_at)}
</span>
</div>
}
sub={fmtIso(scheduler.data?.last_run_at)}
/>
<KeyValue
label="Status"
value={
scheduler.data?.last_run_ok === null ||
scheduler.data?.last_run_ok === undefined ? (
<span className="text-ink-faint">Noch kein Lauf</span>
) : scheduler.data.last_run_ok ? (
<span className="text-success inline-flex items-center gap-1.5">
<CheckCircle2 className="size-3.5" /> Erfolgreich
</span>
) : (
<span className="text-danger inline-flex items-center gap-1.5">
<ShieldAlert className="size-3.5" /> Fehler
</span>
)
}
sub={scheduler.data?.last_error ?? undefined}
/>
</CardBody>
<CardFooter>
<span>
Single-flight: parallele Läufe werden via Semaphore unterdrückt.
</span>
<Badge tone="outline">tokio-cron-scheduler</Badge>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div>
<CardTitle>Ereignisse</CardTitle>
<p className="text-[12.5px] text-ink-mute mt-1">
Operator-Alerts, jüngste zuerst.
</p>
</div>
<Activity className="size-4 text-ink-faint" />
</CardHeader>
<CardBody className="pt-0">
{alerts.isLoading ? (
<AlertSkeletons />
) : (alerts.data?.items.length ?? 0) === 0 ? (
<EmptyAlerts />
) : (
<ul className="-mx-1">
{alerts.data!.items
.slice()
.reverse()
.slice(0, 6)
.map((a, idx) => (
<li
key={idx}
className="px-1.5 py-3 first:pt-1 last:pb-1 border-b border-line last:border-0"
>
<AlertRow entry={a} />
</li>
))}
</ul>
)}
</CardBody>
</Card>
</section>
</div>
</>
);
}
function countByState(items: Schemas["CertificateDto"][]) {
let valid = 0,
expiring = 0,
expired = 0;
for (const c of items) {
if (c.state === "valid") valid += 1;
else if (c.state === "expiring") expiring += 1;
else if (c.state === "expired") expired += 1;
}
return { valid, expiring, expired, pending: 0, total: items.length };
}
type Tone = "success" | "warn" | "danger" | "neutral";
const TONE_DECOR: Record<Tone, { ring: string; iconBg: string; iconColor: string }> = {
success: {
ring: "before:bg-success/60",
iconBg: "bg-success-soft",
iconColor: "text-success",
},
warn: {
ring: "before:bg-warn/70",
iconBg: "bg-warn-soft",
iconColor: "text-warn",
},
danger: {
ring: "before:bg-danger/70",
iconBg: "bg-danger-soft",
iconColor: "text-danger",
},
neutral: {
ring: "before:bg-line-strong",
iconBg: "bg-overlay",
iconColor: "text-ink-mute",
},
};
function StatCard({
label,
value,
hint,
tone,
icon: Icon,
emphasise = false,
}: {
label: string;
value: number | undefined;
hint: string;
tone: Tone;
icon: React.ComponentType<{ className?: string }>;
emphasise?: boolean;
}) {
const t = TONE_DECOR[tone];
return (
<div
className={cn(
"surface relative overflow-hidden p-5",
"before:content-[''] before:absolute before:inset-y-3 before:left-0 before:w-[3px] before:rounded-r-full",
t.ring,
)}
>
<div className="flex items-start justify-between">
<span className="text-[11px] uppercase tracking-[0.08em] text-ink-faint">
{label}
</span>
<div
className={cn(
"size-7 rounded-[8px] grid place-items-center",
t.iconBg,
)}
>
<Icon className={cn("size-3.5", t.iconColor)} />
</div>
</div>
<div className="mt-3 flex items-baseline gap-2">
<span
className={cn(
"font-serif num text-ink",
emphasise ? "text-[56px] leading-none" : "text-[48px] leading-none",
)}
>
{value ?? <Skeleton className="inline-block w-[60px] h-[40px]" />}
</span>
</div>
<p className="mt-2 text-[12px] text-ink-mute">{hint}</p>
</div>
);
}
function KeyValue({
label,
value,
sub,
}: {
label: string;
value: React.ReactNode;
sub?: string;
}) {
return (
<div className="flex flex-col gap-1">
<span className="text-[10.5px] uppercase tracking-[0.08em] text-ink-faint">
{label}
</span>
<div>{value}</div>
{sub && <span className="text-[11.5px] text-ink-faint mono truncate">{sub}</span>}
</div>
);
}
function AlertRow({ entry }: { entry: Schemas["AlertEntry"] }) {
const tone =
entry.severity === "error"
? "danger"
: entry.severity === "warning"
? "warn"
: "accent";
const Icon =
entry.severity === "error"
? AlertCircle
: entry.severity === "warning"
? TriangleAlert
: Info;
return (
<div className="flex gap-3">
<div
className={cn(
"size-7 shrink-0 rounded-[8px] grid place-items-center",
tone === "danger" && "bg-danger-soft text-danger",
tone === "warn" && "bg-warn-soft text-warn",
tone === "accent" && "bg-accent-soft text-accent",
)}
>
<Icon className="size-3.5" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-[12.5px] font-medium text-ink truncate">
{entry.subject}
</span>
<span className="ml-auto text-[10.5px] text-ink-faint shrink-0 mono">
{fmtRelative(entry.at)}
</span>
</div>
<p className="text-[12px] text-ink-mute mt-0.5 line-clamp-2">{entry.body}</p>
</div>
</div>
);
}
function EmptyAlerts() {
return (
<div className="py-8 text-center">
<div className="mx-auto size-9 rounded-full bg-overlay grid place-items-center">
<Info className="size-4 text-ink-faint" />
</div>
<p className="mt-3 text-[13px] text-ink">Keine Ereignisse</p>
<p className="text-[12px] text-ink-mute mt-0.5">
Erfolgreiche Läufe und Alerts erscheinen hier.
</p>
</div>
);
}
function AlertSkeletons() {
return (
<div className="space-y-3">
{[0, 1, 2].map((i) => (
<div key={i} className="flex gap-3">
<Skeleton className="size-7 rounded-[8px]" />
<div className="flex-1 space-y-1.5">
<Skeleton className="h-3 w-2/3" />
<Skeleton className="h-3 w-full" />
</div>
</div>
))}
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
import { Sidebar } from "@/components/layout/sidebar";
import { api } from "@/lib/api";
export const Route = createFileRoute("/_app")({
beforeLoad: async () => {
const { response } = await api.GET("/auth/me");
if (response.status === 401 || response.status === 403) {
throw redirect({ to: "/login" });
}
},
component: AppLayout,
});
function AppLayout() {
return (
<div className="flex min-h-screen">
<Sidebar />
<main className="flex-1 flex flex-col min-w-0">
<Outlet />
</main>
</div>
);
}
+139
View File
@@ -0,0 +1,139 @@
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { ShieldCheck, KeyRound, AlertTriangle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { api, unwrap, ApiError } from "@/lib/api";
export const Route = createFileRoute("/login")({
component: LoginPage,
});
function LoginPage() {
const navigate = useNavigate();
const [devSubject, setDevSubject] = useState("CN=lab.operator,OU=PKI,O=SMGW");
const [err, setErr] = useState<string | null>(null);
const login = useMutation({
mutationFn: () =>
unwrap(
api.POST("/auth/session", {
body: { dev_subject: devSubject },
}),
),
onError: (e: ApiError) => setErr(e.message),
onSuccess: () => navigate({ to: "/" }),
});
return (
<div className="min-h-screen grid lg:grid-cols-[1.05fr_0.95fr]">
<aside className="relative hidden lg:flex flex-col justify-between p-12 border-r border-line overflow-hidden bg-paper">
<div className="absolute inset-0 grid-bg opacity-60 pointer-events-none" />
<div className="relative z-10 flex items-center gap-3">
<div className="size-10 rounded-[12px] bg-ink text-paper grid place-items-center font-display font-semibold text-[17px] tracking-[-0.02em]">
sm
</div>
<div className="leading-tight">
<div className="font-display text-[16px] font-semibold tracking-[-0.015em] text-ink">
SMGW PKI · Console
</div>
<div className="text-[11px] uppercase tracking-[0.1em] text-ink-faint">
Test / Labor BSI TR-03129-4
</div>
</div>
</div>
<div className="relative z-10 max-w-md space-y-6">
<p className="font-serif italic text-[34px] leading-[1.1] text-ink tracking-[-0.01em]">
Eine ruhige Konsole für eine{" "}
<span className="not-italic font-display font-semibold">
vorsichtige PKI
</span>
.
</p>
<p className="text-[13.5px] leading-relaxed text-ink-mute">
Steuerfläche für den smgw-pki-automator. Erneuerungen, Sub-CA-Status,
iconfig-Generierung und SoftHSM-Diagnose alle Operationen sind
auditiert und an mTLS-Identitäten gebunden.
</p>
<div className="flex flex-wrap gap-2">
<Badge tone="outline">TR-03129-4</Badge>
<Badge tone="outline">TR-03109-1</Badge>
<Badge tone="outline">SM-PKI CP</Badge>
</div>
</div>
<div className="relative z-10 text-[11.5px] text-ink-faint mono">
v0.1.0 · build skeleton · {new Date().getFullYear()}
</div>
</aside>
<section className="flex items-center justify-center p-8">
<div className="w-full max-w-[420px] surface p-7">
<div className="flex items-center gap-2 text-[11px] uppercase tracking-[0.1em] text-ink-faint">
<ShieldCheck className="size-3.5 text-accent" />
Sitzung
</div>
<h2 className="mt-3 font-display text-[26px] font-semibold tracking-[-0.02em] text-ink">
Anmeldung über mTLS
</h2>
<p className="mt-1.5 text-[13px] leading-relaxed text-ink-mute">
Ihr Client-Zertifikat wird vom Reverse Proxy terminiert und im Header{" "}
<code className="mono text-[12px]">X-Forwarded-Cert-Subject</code>{" "}
übergeben. Im Lab-Modus ist ein Dev-Subject als Fallback erlaubt.
</p>
<form
onSubmit={(e) => {
e.preventDefault();
setErr(null);
login.mutate();
}}
className="mt-7 space-y-4"
>
<div className="space-y-1.5">
<Label htmlFor="dev_subject">Dev-Subject (nur Lab)</Label>
<Input
id="dev_subject"
value={devSubject}
onChange={(e) => setDevSubject(e.target.value)}
mono
placeholder="CN=…"
/>
<p className="text-[11.5px] text-ink-faint">
Wird ignoriert, sobald der Proxy ein gültiges Cert-Subject mitschickt.
</p>
</div>
{err && (
<div className="flex gap-2 items-start surface-inset p-3 text-[12.5px] text-danger">
<AlertTriangle className="size-4 mt-0.5 shrink-0" />
<span>{err}</span>
</div>
)}
<Button
type="submit"
variant="primary"
size="lg"
className="w-full"
disabled={login.isPending}
>
<KeyRound className="size-4" />
Sitzung erstellen
</Button>
</form>
<div className="mt-6 pt-5 border-t border-line text-[11.5px] text-ink-faint leading-relaxed">
Cookies sind <span className="text-ink">HttpOnly</span> und{" "}
<span className="text-ink">SameSite=Strict</span>. Sitzungsdauer 8 h.
</div>
</div>
</section>
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}
+36
View File
@@ -0,0 +1,36 @@
import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import path from "node:path";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
const apiTarget = env.VITE_API_PROXY_TARGET ?? "http://localhost:8443";
return {
plugins: [
TanStackRouterVite({ target: "react", autoCodeSplitting: true }),
react(),
tailwindcss(),
],
resolve: {
alias: { "@": path.resolve(__dirname, "./src") },
},
server: {
host: "0.0.0.0",
port: 5173,
proxy: {
"/api": {
target: apiTarget,
changeOrigin: true,
secure: false,
},
},
},
build: {
target: "es2022",
sourcemap: true,
},
};
});