feat: migrate Leptos frontend to Next.js 16 (React 19)
Deploy to VPS / deploy (push) Failing after 42s

Complete migration from services/frontend.old/ (Leptos 0.7 WASM + Rust)
to services/frontend/ (Next.js 16 static export + TypeScript + Tailwind v4).

Summary:
- Port all shared types (message, guild, voice, media, dashboard, recording, ui)
- Build fetch-based API client covering all 30+ backend endpoints
- WebSocket client with auto-reconnect (exponential backoff, 20 attempts)
- React context provider for WS with typed event subscription (22 event types)
- Login page with localStorage auth + auto-redirect
- Dashboard layout with sidebar, header (WS status + theme toggle)
- Messages: feed, search, images tab, review tab, channel filter, detail modal
- Live: voice connection, music player, recordings, mic transmit, active speakers
- Dashboard: stats, user list, channel list, detail views
- Mascot chatbot with history + clear
- uiStateApi persistence for selected tab
- Add static export config, update deploy scripts and CI
This commit is contained in:
asepharyana committed 2026-07-26 11:27:21 +07:00
1 parent cbbe939fad
commit 5e01ec0806
165 files changed
+5035 -16410

No files matched your search

-8
View File
@@ -47,14 +47,6 @@ jobs:
version: 11.1.3
run_install: false
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Install Trunk
run: cargo install trunk --version 0.22.0-beta.1 --locked
- name: Install dependencies
run: pnpm install --frozen-lockfile
+17 -38
View File
@@ -12,7 +12,7 @@ Built with **pnpm workspace monorepo** with 3 services and 1 shared library:
|---------|------|-------------|
| `discord-moderation-backend` | `services/backend` | Express HTTP/WS server, REST API, Redis bridge |
| `@bete/discord-gateway` | `services/discord-gateway` | Discord client, voice recording, message capture, AI moderation |
| `frontend` | `services/frontend/frontend` | Leptos 0.7 CSR WASM dashboard |
| `frontend` | `services/frontend` | Next.js 16 (React 19) static dashboard, Tailwind v4, shadcn/ui |
| `@bete/shared` | `packages/shared` | Shared types, errors, logger, utilities |
**Database:** PostgreSQL (Drizzle ORM) — NOT SQLite.
@@ -28,7 +28,7 @@ Discord
|
v
discord-gateway ---- Redis ---- backend ---- WebSocket ---- frontend
| pub/sub (broadcast) (Leptos WASM)
| pub/sub (broadcast) (Next.js static)
| |
| |
<------------------+
@@ -194,43 +194,22 @@ The core service that connects to Discord using `discord.js-selfbot-v13`.
- `src/shared/database/voiceRecordingRepo.ts` — Voice recording queries
- `src/shared/discord/clientOptions.ts` — Discord client configuration
### frontend (`services/frontend/frontend`)
### frontend (`services/frontend`)
Leptos 0.7 CSR WASM + TypeScript + CSS dashboard.
Next.js 16 (React 19) static export dashboard, built with TypeScript + Tailwind v4 + shadcn/ui + base-ui.
**Tech stack:**
- Leptos 0.7 CSR (WASM via `#[wasm_bindgen(start)]`)
- Trunk for bundling
- Plain CSS with design tokens
- Canvas 2D via `web-sys` for audio visualization
- CSS animations (GSAP/Framer Motion dihapus — unused)
- `web-sys` primitives (WebSocket, AudioContext, IntersectionObserver)
- `lucide-leptos` 3 for icons
- Next.js 16 (App Router, static export)
- React 19 with React Compiler
- TypeScript strict
- Tailwind v4 + shadcn/ui + base-ui components
- lucide-react icons
**Feature structure (entity + feature slices):**
- `entities/` — Type exports re-exported from shared API client
- `guild/types.ts` — Guild, Channel
- `message/types.ts` — MessageRecord, PageResult
- `voice/types.ts` — ActiveSpeaker, VoiceStatus
- `media/types.ts` — MediaItem, MediaMode, MediaState
- `ui/types.ts` — UIState, DashboardTab
- `features/`
- `live/` — Voice connection, music player, screenshare, recordings
- Components: ActiveSpeakers, AudioVisualizer, MusicSubPanel, NowPlaying, RecordingsSubPanel, ScreenSubPanel, VoiceConnectionCard
- Hooks: `useVoiceControl`, `useMediaControl`
- `messages/` — Message list with filters
- Hooks: `useMessages`
- `shared/`
- `api/client.ts` — All HTTP API calls + types
- `ws/socket.ts` — WebSocket singleton with `useDashboardSocket` hook
- `ws/events.ts` — Typed event map
- `hooks/` — useAudioPlayback, useAudioTransmit, useUIState, useMascotChat, useMascotSummary, useFramerStagger, useGsapTransition, useLocalStorage
- `ui/` — Reusable UI components (Badge, Button, Card, Input, Select, Skeleton, Tabs, Toast, ScrollArea)
- `lib/utils.ts` — `cn()` and other utilities
**WebSocket protocol:**
- Binary: PCM audio data (24kHz mono s16le)
- JSON events: message_*, voice_*, attachment_*, user_state, ui_state, media_state, heartbeat
**Feature structure:**
- `src/app/` — App Router pages (login, dashboard with tabs)
- `src/features/` — Feature components (dashboard, messages, live, mascot)
- `src/lib/` — Shared utilities (types, API client, WebSocket, hooks)
- `src/components/` — Shared UI components (layout, ui)
### shared (`packages/shared`)
@@ -388,16 +367,16 @@ pnpm install
# Run each service in development mode (separate terminal each)
pnpm run dev:backend # Backend on port 3001
pnpm run dev:discord-gateway # Discord client + all features
pnpm run dev:web # Frontend via trunk serve
pnpm run dev:web # Frontend via next dev (port 3000)
# Build
pnpm run build:backend
pnpm run build:discord-gateway
pnpm run build:web # trunk build --release
pnpm run build:web # next build (static export)
# Type checking
pnpm run typecheck # Node services (pnpm -r)
pnpm run typecheck:web # Leptos frontend (cargo check)
pnpm run typecheck:web # Frontend typecheck (next build)
# Lint (Biome)
pnpm run lint
+5 -5
View File
@@ -9,7 +9,7 @@
#
# Usage:
# ./deploy.sh # build + deploy all services
# ./deploy.sh --frontend # frontend WASM only
# ./deploy.sh --frontend # frontend (Next.js) only
# ./deploy.sh --backend # backend JS only
# ./deploy.sh --gateway # discord-gateway JS only
# ./deploy.sh --all # same as no-flag (default)
@@ -32,7 +32,7 @@ APP_DIR="/opt/imphenbot"
COMPOSE_FILE="infra/docker/docker-compose.yml"
# Local build output directories (relative to repo root)
FRONTEND_DIST="services/frontend/frontend/dist"
FRONTEND_DIST="services/frontend/out"
BACKEND_DIST="services/backend/dist"
GATEWAY_DIST="services/discord-gateway/dist"
@@ -118,9 +118,9 @@ if $DO_BUILD; then
fi
if $DO_FRONTEND; then
log "Building frontend (WASM)..."
cd services/frontend/frontend
trunk build --release 2>&1 | tail -5 || die "Frontend build failed"
log "Building frontend (Next.js)..."
cd services/frontend
bun run build 2>&1 | tail -5 || die "Frontend build failed"
cd "$REPO_ROOT"
ok "Frontend built"
fi
+3 -3
View File
@@ -7,9 +7,9 @@
"scripts": {
"dev:backend": "pnpm --filter './services/backend' run dev",
"dev:discord-gateway": "pnpm --filter './services/discord-gateway' run dev",
"dev:web": "cd services/frontend/frontend && trunk serve --address 0.0.0.0",
"build:web": "cd services/frontend/frontend && trunk build --release",
"typecheck:web": "cd services/frontend/frontend && cargo check",
"dev:web": "pnpm --filter './services/frontend' run dev",
"build:web": "pnpm --filter './services/frontend' run build",
"typecheck:web": "cd services/frontend && bun run build",
"build:backend": "pnpm --filter './services/backend' run build",
"build:discord-gateway": "pnpm --filter './services/discord-gateway' run build",
"typecheck": "pnpm -r run typecheck",
-3
View File
@@ -1,3 +0,0 @@
# API/WS endpoints — set these before building
VITE_BE_API_URL=http://localhost:3001
VITE_BE_WS_URL=ws://localhost:3001/ws
+41
View File
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+5
View File
@@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
-2557
View File
File diff suppressed because it is too large. Load diff
-3
View File
@@ -1,3 +0,0 @@
[workspace]
resolver = "2"
members = ["shared-types", "frontend"]
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+53
View File
@@ -0,0 +1,53 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noUnknownAtRules": "off",
"useIterableCallbackReturn": "off",
"noArrayIndexKey": "warn"
},
"a11y": {
"useButtonType": "off",
"noAutofocus": "off"
},
"performance": {
"noImgElement": "warn"
},
"security": {
"noDangerouslySetInnerHtml": "warn"
},
"correctness": {
"noInvalidUseBeforeDeclaration": "off",
"noUnusedFunctionParameters": "warn"
}
},
"domains": {
"next": "recommended",
"react": "recommended"
}
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
+881
View File
@@ -0,0 +1,881 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "frontend",
"dependencies": {
"@base-ui/react": "^1.6.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.27.0",
"next": "16.2.12",
"react": "19.2.4",
"react-dom": "19.2.4",
"shadcn": "^4.15.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"tailwindcss": "^4",
"typescript": "^5",
},
},
},
"trustedDependencies": [
"sharp",
],
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3",Line truncated
"@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "shLine truncated
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="],
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
"@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="],
"@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="],
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkLine truncated
"@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="],
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
"@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
"@base-ui/react": ["@base-ui/react@1.6.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.3.1", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-/jzjTWJYXhRFO45BevLine truncated
"@base-ui/utils": ["@base-ui/utils@0.3.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg=="],
"@biomejs/biome": ["@biomejs/biome@2.2.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.0", "@biomejs/cli-darwin-x64": "2.2.0", "@biomejs/cli-linux-arm64": "2.2.0", "@biomejs/cli-linux-arm64-musl": "2.2.0", "@biomejs/cli-linux-x64": "2.2.0", "@biomejs/cli-linux-x64-musl": "2.2.0", "@biomejs/cli-win32-arm64": "2.2.0", "@biomejs/cli-win32-x64": "2.2.0" }, "bin": { "biome": "bin/biome" } }, "sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMLine truncated
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww=="],
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.75.1", "", { "dependencies": { "@dotenvx/primitives": "^0.8.0", "commander": "^11.1.0", "conf": "^10.2.0", "dotenv": "^17.2.1", "enquirer": "^2.4.1", "env-paths": "^2.2.1", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "open": "^8.4.2", "picomatch": "^4.0.4", "systeminformation": "^5.22.11", "undici": "^7.11.0", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512Line truncated
"@dotenvx/primitives": ["@dotenvx/primitives@0.8.0", "", {}, "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
"@hono/node-server": ["@hono/node-server@1.19.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="],
"@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=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-jLine truncated
"@next/env": ["@next/env@16.2.12", "", {}, "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.12", "", { "os": "win32", "cpu": "x64" }, "sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "Line truncated
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="],
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
"@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"atomically": ["atomically@1.7.0", "", {}, "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="],
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.3", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sbT0Ui/CZwyAyy7icT1Gw5P1LKRlFaHwaF6tDCW5YHq2X5SeeZFphBuIagopSfwSSZq3sQcbmEL072yphxm7ew=="],
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
"brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"conf": ["conf@10.2.0", "", { "dependencies": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", "atomically": "^1.7.0", "debounce-fn": "^4.0.0", "dot-prop": "^6.0.1", "env-paths": "^2.2.1", "json-schema-typed": "^7.0.3", "onetime": "^5.1.2", "pkg-up": "^3.1.0", "semver": "^7.3.5" } }, "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg=="],
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debounce-fn": ["debounce-fn@4.0.0", "", { "dependencies": { "mimic-fn": "^3.0.0" } }, "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
"dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"electron-to-chromium": ["electron-to-chromium@1.5.396", "", {}, "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"enhanced-resolve": ["enhanced-resolve@5.24.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ=="],
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.Line truncated
"express-rate-limit": ["express-rate-limit@8.6.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="],
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"hono": ["hono@4.12.32", "", {}, "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
"iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ip-address": ["ip-address@10.3.1", "", {}, "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-obj": ["is-obj@3.0.0", "", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="],
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"jose": ["jose@6.2.4", "", {}, "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"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", "lightningcsLine truncated
"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=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@1.27.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-rJicGl/3Fly/E0rOH1YmPZ6e49JCnKknh1ox1vpHnkfjujAkKA6sqUZvH3MTAaXXjgexyUwgNwTJzTtYuAFYJw=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"mimic-fn": ["mimic-fn@3.1.0", "", {}, "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="],
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"next": ["next@16.2.12", "", { "dependencies": { "@next/env": "16.2.12", "@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.12", "@next/swc-darwin-x64": "16.2.12", "@next/swc-linux-arm64-gnu": "16.2.12", "@next/swc-linux-arm64-musl": "16.2.12", "@next/swc-linux-x64-gnu": "16.2.12", "@next/swc-linux-x64-musl": "16.2.12", "@next/swc-win32-arm64Line truncated
"node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="],
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
"postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="],
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"recast": ["recast@0.23.12", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shadcn": ["shadcn@4.15.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-eLine truncated
"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-libviLine truncated
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "@babel/core": "*", "babel-plugin-macros": "*", "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "optionalPeers": ["@babel/core", "babel-plugin-macros"] }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"systeminformation": ["systeminformation@5.33.1", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w=="],
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="],
"tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"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=="],
"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=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="],
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"@dotenvx/dotenvx/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
"enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
"micromatch/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=="],
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
"@dotenvx/dotenvx/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"@dotenvx/dotenvx/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"@dotenvx/dotenvx/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"@dotenvx/dotenvx/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"@dotenvx/dotenvx/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
-63
View File
@@ -1,63 +0,0 @@
[package]
name = "frontend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
leptos = { version = "=0.9.0-alpha", features = ["csr"] }
leptos-use = "0.19"
lucide-leptos = "3.23"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
"WebSocket",
"MessageEvent",
"CloseEvent",
"ErrorEvent",
"CanvasRenderingContext2d",
"AudioContext",
"AudioBuffer",
"AudioBufferSourceNode",
"AudioDestinationNode",
"AudioNode",
"AudioProcessingEvent",
"MediaStreamAudioSourceNode",
"ScriptProcessorNode",
"Window",
"Document",
"Element",
"HtmlElement",
"HtmlSelectElement",
"KeyboardEvent",
"Storage",
"IntersectionObserver",
"ResizeObserver",
"Url",
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
"HtmlInputElement",
"HtmlAudioElement",
"HtmlCanvasElement",
"MediaDevices",
"MediaStream",
"MediaStreamConstraints",
"MediaStreamTrack",
"Navigator",
"console",
] }
gloo-net = "0.6"
gloo-timers = { version = "0.3", features = ["futures"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde-wasm-bindgen = "0.6"
wasm-logger = "0.2"
console_error_panic_hook = "0.1"
regex = "1"
shared-types = { path = "../shared-types" }
-7
View File
@@ -1,7 +0,0 @@
[build]
target = "index.html"
dist = "dist"
[serve]
port = 8080
open = false
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#3b82f6" />
<title>IMPHNEN -- Discord Moderation</title>
<link data-trunk rel="rust" data-crate="frontend" data-wasm="frontend.wasm" />
<link data-trunk rel="css" href="src/styles/bundle.css" />
<link data-trunk rel="copy-dir" href="public/" />
<!-- Inter + JetBrains Mono fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
<!-- Preload WASM -->
<link rel="preload" href="/frontend.wasm" as="fetch" crossorigin="anonymous" />
</head>
<body>
</body>
</html>
@@ -1 +0,0 @@
# Trunk copies this directory to dist/
@@ -1,24 +0,0 @@
use crate::api::client::{request, ApiError};
use serde::{Deserialize, Serialize};
use crate::{log_error, log_info, log_warn, make_logger};
make_logger!();
#[derive(Serialize)]
struct LoginPayload {
password: String,
}
#[derive(Deserialize)]
struct LoginResponse {
ok: bool,
}
pub async fn login(password: &str) -> Result<bool, ApiError> {
let payload = LoginPayload {
password: password.to_string(),
};
let body = serde_json::to_string(&payload).unwrap();
let resp: LoginResponse = request("POST", "/api/auth/login", Some(&body)).await?;
Ok(resp.ok)
}
@@ -1,184 +0,0 @@
use serde::de::DeserializeOwned;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Headers, Request, RequestInit, RequestMode, Response};
use crate::{log_debug, log_error, make_logger};
make_logger!();
#[derive(Debug)]
pub struct ApiError {
pub message: String,
pub status_code: u16,
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "API error {}: {}", self.status_code, self.message)
}
}
impl std::error::Error for ApiError {}
fn get_base_url() -> String {
if let Some(window) = web_sys::window() {
let location = window.location();
let protocol = location.protocol().unwrap_or_else(|_| "http:".to_string());
let protocol = protocol.trim_end_matches(':');
let host = location
.host()
.unwrap_or_else(|_| "localhost:3001".to_string());
format!("{}://{}", protocol, host)
} else {
"http://localhost:3001".to_string()
}
}
fn get_auth_header() -> Option<String> {
// Read password from sessionStorage
let storage = web_sys::window()?.local_storage().ok()??;
storage.get_item("admin-password").ok()?
}
pub async fn request<T: DeserializeOwned>(
method: &str,
path: &str,
body: Option<&str>,
) -> Result<T, ApiError> {
let url = format!("{}{}", get_base_url(), path);
let headers = Headers::new().map_err(|_| {
let msg = "Failed to create headers";
log_error!("API {} {} failed: {}", method, path, msg);
ApiError {
message: msg.to_string(),
status_code: 0,
}
})?;
if let Some(password) = get_auth_header() {
headers.set("X-Admin-Password", &password).ok();
}
if body.is_some() {
headers.set("Content-Type", "application/json").ok();
}
log_debug!("{} {} ->", method, path);
let opts = RequestInit::new();
opts.set_method(method);
opts.set_headers(&headers);
opts.set_mode(RequestMode::Cors);
if let Some(json_body) = body {
opts.set_body(&JsValue::from_str(json_body));
}
let request = Request::new_with_str_and_init(&url, &opts).map_err(|e| {
let msg = format!("Failed to create request: {:?}", e);
log_error!("API {} {} failed: {}", method, path, msg);
ApiError {
message: msg,
status_code: 0,
}
})?;
let window = web_sys::window().ok_or_else(|| {
let msg = "No window";
log_error!("API {} {} failed: {}", method, path, msg);
ApiError {
message: msg.to_string(),
status_code: 0,
}
})?;
let resp_value = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(|e| {
let msg = format!("Fetch failed: {:?}", e);
log_error!("API {} {} failed: {}", method, path, msg);
ApiError {
message: msg,
status_code: 0,
}
})?;
let response: Response = resp_value.dyn_into().map_err(|_| {
let msg = "Invalid response";
log_error!("API {} {} failed: {}", method, path, msg);
ApiError {
message: msg.to_string(),
status_code: 0,
}
})?;
let status = response.status();
if status >= 400 {
let text = JsFuture::from(response.text().map_err(|_| {
let msg = "Failed to read error body";
log_error!("API {} {} failed: {}", method, path, msg);
ApiError {
message: msg.to_string(),
status_code: status,
}
})?)
.await
.ok()
.and_then(|v| v.as_string())
.unwrap_or_default();
log_error!("API {} {} failed: status={} {}", method, path, status, text);
return Err(ApiError {
message: text,
status_code: status,
});
}
let text = JsFuture::from(response.text().map_err(|_| {
let msg = "Failed to read response body";
log_error!("API {} {} failed: {}", method, path, msg);
ApiError {
message: msg.to_string(),
status_code: status,
}
})?)
.await
.map_err(|_| {
let msg = "Failed to await response";
log_error!("API {} {} failed: {}", method, path, msg);
ApiError {
message: msg.to_string(),
status_code: status,
}
})?
.as_string()
.ok_or_else(|| {
let msg = "Response is not text";
log_error!("API {} {} failed: {}", method, path, msg);
ApiError {
message: msg.to_string(),
status_code: status,
}
})?;
log_debug!("{} {} <- {}", method, path, status);
serde_json::from_str(&text).map_err(|e| {
let msg = format!(
"JSON parse error: {} — body: {}",
e,
&text[..text.len().min(200)]
);
log_error!("API {} {} failed: {}", method, path, msg);
ApiError {
message: msg,
status_code: status,
}
})
}
pub async fn request_no_body(method: &str, path: &str) -> Result<(), ApiError> {
request::<serde_json::Value>(method, path, None).await?;
Ok(())
}
@@ -1,12 +0,0 @@
use crate::api::client::{request, ApiError};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppConfigResponse {
pub monitor_guild_id: Option<String>,
}
/// GET /api/config
pub async fn get_config() -> Result<AppConfigResponse, ApiError> {
request("GET", "/api/config", None).await
}
@@ -1,96 +0,0 @@
use crate::api::client::{request, ApiError};
use shared_types::dashboard::*;
use crate::{log_debug, make_logger};
make_logger!();
/// GET /api/dashboard/stats
pub async fn get_dashboard_stats() -> Result<DashboardStats, ApiError> {
log_debug!("get_dashboard_stats");
request("GET", "/api/dashboard/stats", None).await
}
/// GET /api/dashboard/users?limit=&cursor=&search=
pub async fn get_dashboard_users(
limit: Option<u32>,
cursor: Option<&str>,
search: Option<&str>,
) -> Result<PaginatedUsers, ApiError> {
log_debug!("get_dashboard_users: limit={:?}, cursor={:?}, search={:?}", limit, cursor, search);
let mut path = "/api/dashboard/users".to_string();
let mut params = vec![];
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
if let Some(c) = cursor {
params.push(format!("cursor={}", c));
}
if let Some(s) = search {
params.push(format!("search={}", s));
}
if !params.is_empty() {
path.push_str(&format!("?{}", params.join("&")));
}
request("GET", &path, None).await
}
#[derive(serde::Deserialize)]
pub struct PaginatedUsers {
pub data: Vec<DashboardUser>,
#[serde(rename = "nextCursor")]
pub next_cursor: Option<String>,
}
/// GET /api/dashboard/users/{userId}
pub async fn get_dashboard_user_detail(user_id: &str) -> Result<DashboardUserDetail, ApiError> {
log_debug!("get_dashboard_user_detail: user_id={}", user_id);
request("GET", &format!("/api/dashboard/users/{}", user_id), None).await
}
/// GET /api/dashboard/channels?limit=&cursor=&search=&guild_id=
pub async fn get_dashboard_channels(
limit: Option<u32>,
cursor: Option<&str>,
search: Option<&str>,
guild_id: Option<&str>,
) -> Result<PaginatedChannels, ApiError> {
log_debug!("get_dashboard_channels: limit={:?}, cursor={:?}, search={:?}, guild_id={:?}", limit, cursor, search, guild_id);
let mut path = "/api/dashboard/channels".to_string();
let mut params = vec![];
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
if let Some(c) = cursor {
params.push(format!("cursor={}", c));
}
if let Some(s) = search {
params.push(format!("search={}", s));
}
if let Some(g) = guild_id {
params.push(format!("guild_id={}", g));
}
if !params.is_empty() {
path.push_str(&format!("?{}", params.join("&")));
}
request("GET", &path, None).await
}
#[derive(serde::Deserialize)]
pub struct PaginatedChannels {
pub data: Vec<DashboardChannel>,
#[serde(rename = "nextCursor")]
pub next_cursor: Option<String>,
}
/// GET /api/dashboard/channels/{channelId}
pub async fn get_dashboard_channel_detail(
channel_id: &str,
) -> Result<DashboardChannelDetail, ApiError> {
log_debug!("get_dashboard_channel_detail: channel_id={}", channel_id);
request(
"GET",
&format!("/api/dashboard/channels/{}", channel_id),
None,
)
.await
}
@@ -1,32 +0,0 @@
use crate::api::client::{request, ApiError};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize)]
struct MascotChatRequest<'a> {
message: &'a str,
}
#[derive(Debug, Deserialize)]
pub struct MascotChatResponse {
pub response: String,
pub timestamp: String,
}
#[derive(Debug, Deserialize)]
pub struct ChatHistoryMessage {
pub role: String,
pub content: String,
pub timestamp: String,
}
pub async fn send_mascot_message(message: &str) -> Result<MascotChatResponse, ApiError> {
let body = serde_json::to_string(&MascotChatRequest { message }).map_err(|err| ApiError {
message: format!("Failed to serialize mascot request: {}", err),
status_code: 0,
})?;
request("POST", "/api/mascot/chat", Some(&body)).await
}
/// GET /api/mascot/chat/history
pub async fn get_chat_history() -> Result<Vec<ChatHistoryMessage>, ApiError> {
request("GET", "/api/mascot/chat/history", None).await
}
@@ -1,109 +0,0 @@
use crate::api::client::{request, ApiError};
use shared_types::message::{MessageRecord, PageResult};
use crate::{log_debug, make_logger};
make_logger!();
/// GET /api/messages?guildId=&limit=&channelId=&cursor=
pub async fn get_messages(
guild_id: &str,
limit: Option<u32>,
channel_id: Option<&str>,
cursor: Option<&str>,
) -> Result<PageResult<MessageRecord>, ApiError> {
log_debug!("get_messages: guild_id={}, limit={:?}, channel_id={:?}, cursor={:?}", guild_id, limit, channel_id, cursor);
let mut path = format!("/api/messages?guildId={}", guild_id);
if let Some(l) = limit {
path.push_str(&format!("&limit={}", l));
}
if let Some(c) = channel_id {
path.push_str(&format!("&channelId={}", c));
}
if let Some(c) = cursor {
path.push_str(&format!("&cursor={}", c));
}
request("GET", &path, None).await
}
/// GET /api/review?params
/// Backend `GET /review` accepts `channelId` and `limit` (not guildId).
pub async fn get_review_messages(
limit: Option<u32>,
channel_id: Option<&str>,
) -> Result<PageResult<MessageRecord>, ApiError> {
log_debug!("get_review_messages: limit={:?}, channel_id={:?}", limit, channel_id);
let mut path = "/api/review".to_string();
let mut params = vec![];
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
if let Some(c) = channel_id {
params.push(format!("channelId={}", c));
}
if !params.is_empty() {
path.push_str(&format!("?{}", params.join("&")));
}
request("GET", &path, None).await
}
/// GET /api/messages/images?guildId=&limit=
pub async fn get_images(
guild_id: &str,
limit: Option<u32>,
) -> Result<PageResult<MessageRecord>, ApiError> {
log_debug!("get_images: guild_id={}, limit={:?}", guild_id, limit);
let mut path = format!("/api/messages/images?guildId={}", guild_id);
if let Some(l) = limit {
path.push_str(&format!("&limit={}", l));
}
request("GET", &path, None).await
}
/// GET /api/messages/detail/{id}
pub async fn get_message_detail(id: &str) -> Result<Option<MessageRecord>, ApiError> {
log_debug!("get_message_detail: id={}", id);
request("GET", &format!("/api/messages/detail/{}", id), None).await
}
/// POST /api/messages/{id}/reanalyze
pub async fn reanalyze_message(id: &str) -> Result<(), ApiError> {
log_debug!("reanalyze_message: id={}", id);
let _: serde_json::Value = request(
"POST",
&format!("/api/messages/{}/reanalyze", id),
Some("{}"),
)
.await?;
Ok(())
}
/// POST /api/messages/reanalyze-batch
pub async fn reanalyze_batch() -> Result<u64, ApiError> {
log_debug!("reanalyze_batch");
#[derive(serde::Deserialize)]
#[allow(dead_code)]
struct BatchResp {
ok: bool,
count: u64,
}
let resp: BatchResp = request("POST", "/api/messages/reanalyze-batch", Some("{}")).await?;
Ok(resp.count)
}
/// GET /api/analysis/search?q=&limit=
pub async fn search_messages(
query: &str,
limit: Option<u32>,
) -> Result<Vec<MessageRecord>, ApiError> {
log_debug!("search_messages: query={}, limit={:?}", query, limit);
#[derive(serde::Deserialize)]
struct SearchResult {
results: Vec<MessageRecord>,
}
let mut path = format!("/api/analysis/search?q={}", query);
if let Some(l) = limit {
path.push_str(&format!("&limit={}", l));
}
let resp: SearchResult = request("GET", &path, None).await?;
Ok(resp.results)
}
@@ -1,8 +0,0 @@
pub mod auth;
pub mod client;
pub mod config;
pub mod dashboard;
pub mod mascot;
pub mod messages;
pub mod recordings;
pub mod voice;
@@ -1,25 +0,0 @@
use crate::api::client::{request, request_no_body, ApiError};
use shared_types::recording::VoiceRecordingListResponse;
/// GET /api/recordings?limit=&cursor=
pub async fn get_recordings(
limit: Option<u32>,
cursor: Option<&str>,
) -> Result<VoiceRecordingListResponse, ApiError> {
let mut path = "/api/recordings".to_string();
let mut params = vec![];
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
if let Some(c) = cursor {
params.push(format!("cursor={}", c));
}
if !params.is_empty() {
path.push_str(&format!("?{}", params.join("&")));
}
request("GET", &path, None).await
}
/// DELETE /api/recordings/{id}
pub async fn delete_recording(id: &str) -> Result<(), ApiError> {
request_no_body("DELETE", &format!("/api/recordings/{}", id)).await
}
@@ -1,90 +0,0 @@
use crate::api::client::{request, ApiError};
use serde::Serialize;
use shared_types::guild::{Channel, Guild};
use shared_types::media::MediaState;
use shared_types::voice::VoiceStatus;
/// GET /api/guilds
pub async fn get_guilds() -> Result<Vec<Guild>, ApiError> {
request("GET", "/api/guilds", None).await
}
/// GET /api/guilds/{guildId}/voice-channels
pub async fn get_voice_channels(guild_id: &str) -> Result<Vec<Channel>, ApiError> {
request(
"GET",
&format!("/api/guilds/{}/voice-channels", guild_id),
None,
)
.await
}
/// GET /api/guilds/{guildId}/channels
pub async fn get_text_channels(guild_id: &str) -> Result<Vec<Channel>, ApiError> {
request("GET", &format!("/api/guilds/{}/channels", guild_id), None).await
}
/// GET /api/voice/status
pub async fn get_voice_status() -> Result<VoiceStatus, ApiError> {
request("GET", "/api/voice/status", None).await
}
/// POST /api/voice/connect { guildId, channelId }
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ConnectPayload {
guild_id: String,
channel_id: String,
}
pub async fn connect_voice(guild_id: &str, channel_id: &str) -> Result<VoiceStatus, ApiError> {
let body = serde_json::to_string(&ConnectPayload {
guild_id: guild_id.to_string(),
channel_id: channel_id.to_string(),
})
.unwrap();
request("POST", "/api/voice/connect", Some(&body)).await
}
/// POST /api/voice/disconnect
pub async fn disconnect_voice() -> Result<VoiceStatus, ApiError> {
request("POST", "/api/voice/disconnect", Some("{}")).await
}
/// GET /api/media/status
pub async fn get_media_status() -> Result<MediaState, ApiError> {
request("GET", "/api/media/status", None).await
}
/// POST /api/media/queue { source, mode }
#[derive(Serialize)]
struct MediaQueuePayload {
source: String,
mode: String,
}
pub async fn media_queue(source: &str, mode: &str) -> Result<MediaState, ApiError> {
let body = serde_json::to_string(&MediaQueuePayload {
source: source.to_string(),
mode: mode.to_string(),
})
.unwrap();
request("POST", "/api/media/queue", Some(&body)).await
}
/// POST /api/media/skip
pub async fn media_skip() -> Result<MediaState, ApiError> {
request("POST", "/api/media/skip", Some("{}")).await
}
/// POST /api/media/stop
pub async fn media_stop() -> Result<MediaState, ApiError> {
request("POST", "/api/media/stop", Some("{}")).await
}
/// POST /api/media/volume { volume }
#[derive(Serialize)]
struct VolumePayload {
volume: f64,
}
pub async fn media_volume(volume: f64) -> Result<MediaState, ApiError> {
let body = serde_json::to_string(&VolumePayload { volume }).unwrap();
request("POST", "/api/media/volume", Some(&body)).await
}
-130
View File
@@ -1,130 +0,0 @@
use crate::api::config as config_api;
use crate::features::dashboard::DashboardPanel;
use crate::features::live::LivePanel;
use crate::features::messages::MessagesPanel;
use crate::features::polish::components::{MascotChatbot, ParticleBackground};
use crate::features::polish::{initial_theme, ThemeContext};
use crate::layout::sidebar::Sidebar;
use crate::ws::context::WsContext;
use leptos::prelude::*;
use shared_types::ui_state::Tab;
use wasm_bindgen_futures::spawn_local;
use crate::{log_info, log_warn, make_logger};
make_logger!();
fn get_ws_url() -> String {
web_sys::window()
.map(|w| {
let loc = w.location();
let protocol = loc.protocol().unwrap_or_else(|_| "http:".to_string());
let host = loc.host().unwrap_or_else(|_| "localhost:3001".to_string());
let ws_proto = if protocol.starts_with("https") {
"wss"
} else {
"ws"
};
format!("{}://{}/ws", ws_proto, host)
})
.unwrap_or_else(|| "ws://localhost:3001/ws".to_string())
}
#[derive(Clone)]
pub struct AppConfig {
pub monitor_guild_id: RwSignal<Option<String>>,
}
#[derive(Clone)]
pub struct AuthContext {
pub authenticated: RwSignal<bool>,
pub password: RwSignal<String>,
}
#[derive(Clone)]
pub struct UiContext {
pub active_tab: RwSignal<Tab>,
pub selected_guild: RwSignal<Option<String>>,
}
#[component]
pub fn App() -> impl IntoView {
let auth = AuthContext {
authenticated: RwSignal::new(false),
password: RwSignal::new(String::new()),
};
let ui = UiContext {
active_tab: RwSignal::new(Tab::Messages),
selected_guild: RwSignal::new(None),
};
let theme = ThemeContext {
theme: RwSignal::new(initial_theme()),
};
provide_context(auth.clone());
provide_context(ui.clone());
provide_context(theme.clone());
let config = AppConfig {
monitor_guild_id: RwSignal::new(None),
};
provide_context(config.clone());
let ws = WsContext::new(&get_ws_url());
provide_context(ws.clone());
ws.connect();
log_info!("App mounted, WS connecting to {}", get_ws_url());
spawn_local({
let config = config.clone();
async move {
match config_api::get_config().await {
Ok(cfg) => {
log_info!("[config] fetched OK — monitorGuildId={:?}", cfg.monitor_guild_id);
config.monitor_guild_id.set(cfg.monitor_guild_id);
}
Err(e) => {
log_warn!("[config] failed to fetch: {}", e);
}
}
}
});
Effect::new(move |_| {
if auth.authenticated.get() {
spawn_local({
let config = config.clone();
async move {
match config_api::get_config().await {
Ok(cfg) => {
config.monitor_guild_id.set(cfg.monitor_guild_id);
}
Err(e) => {
log_info!("[config] fetch after auth failed: {}", e);
}
}
}
});
}
});
view! {
<div data-theme=move || theme.theme.get()>
<ParticleBackground />
<div class="app-shell">
<Sidebar />
<div class="app-content">
{move || match ui.active_tab.get() {
Tab::Messages => view! { <MessagesPanel /> }.into_any(),
Tab::Live => view! { <LivePanel /> }.into_any(),
Tab::Dashboard => view! { <DashboardPanel /> }.into_any(),
}}
</div>
</div>
{move || auth.authenticated.get().then(|| view! { <MascotChatbot /> })}
</div>
}
}
-113
View File
@@ -1,113 +0,0 @@
use crate::api::auth as auth_api;
use crate::app::AuthContext;
use crate::app::UiContext;
use leptos::prelude::*;
use shared_types::ui_state::Tab;
use wasm_bindgen_futures::spawn_local;
use crate::{log_error, log_info, log_warn, make_logger};
make_logger!();
#[component]
pub fn AuthOverlay() -> impl IntoView {
let auth = use_context::<AuthContext>().expect("AuthContext not provided");
let ui = use_context::<UiContext>();
let (password, set_password) = signal(String::new());
let (error, set_error) = signal(Option::<String>::None);
let (loading, set_loading) = signal(false);
let handle_submit = move |ev: leptos::ev::SubmitEvent| {
ev.prevent_default();
let pwd = password.get();
if pwd.is_empty() {
set_error.set(Some("Password diperlukan".to_string()));
return;
}
set_loading.set(true);
set_error.set(None);
let auth_clone = auth.clone();
let pwd_clone = pwd.clone();
let set_loading_clone = set_loading;
let set_error_clone = set_error;
spawn_local(async move {
match auth_api::login(&pwd_clone).await {
Ok(true) => {
log_info!("Auth login successful");
if let Some(storage) = web_sys::window()
.and_then(|w| w.local_storage().ok())
.flatten()
{
let _ = storage.set_item("admin-password", &pwd_clone);
}
auth_clone.authenticated.set(true);
auth_clone.password.set(pwd_clone);
}
Ok(false) => {
log_warn!("Auth login failed - wrong password");
set_error_clone.set(Some("Login gagal — password salah".to_string()));
}
Err(e) => {
log_error!("Auth login error: {}", e.message);
set_error_clone.set(Some(format!("Error: {}", e.message)));
}
}
set_loading_clone.set(false);
});
};
let tab_messages = ui.as_ref().map(|u| u.active_tab);
let skip_dismiss = move |_| {
if let Some(ref t) = tab_messages {
t.set(Tab::Messages);
}
};
view! {
<div class="modal-overlay" on:click=skip_dismiss>
<div class="modal-content auth-box" on:click=move |ev| ev.stop_propagation()>
<div class="modal-body" style="position:relative">
<button
class="auth-close-btn"
on:click=skip_dismiss
title="Tutup"
>"×"</button>
<div class="auth-lock">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
</div>
<h2 class="auth-title">"Akses Dashboard"</h2>
<p class="auth-desc">"Masukkan password admin untuk melanjutkan"</p>
<form class="auth-form" on:submit=handle_submit>
<input
type="password"
class="input"
placeholder="Password"
prop:value=password
on:input=move |ev| set_password.set(event_target_value(&ev))
/>
{move || error.get().map(|e| view! {
<p class="auth-error">{e}</p>
})}
<button
type="submit"
class="btn btn-primary btn-lg"
disabled=move || loading.get()
>
{move || if loading.get() { "Memproses..." } else { "Masuk" }}
</button>
<button
on:click=skip_dismiss
class="btn btn-ghost btn-sm"
>
"Lihat dashboard saja"
</button>
</form>
</div>
</div>
</div>
}
}
@@ -1,143 +0,0 @@
use leptos::prelude::*;
use shared_types::dashboard::DashboardChannel;
#[component]
pub fn ChannelSummaryList(
channels: Vec<DashboardChannel>,
loading: bool,
error: Option<String>,
search: String,
has_more: bool,
on_search_change: Box<dyn Fn(String) + Send + Sync + 'static>,
on_load_more: Box<dyn Fn() + Send + Sync + 'static>,
on_retry: Box<dyn Fn() + Send + Sync + 'static>,
) -> impl IntoView {
let search_cb = StoredValue::new(on_search_change);
let load_more_cb = StoredValue::new(on_load_more);
let retry_cb = StoredValue::new(on_retry);
view! {
<div class="card dashboard-list-card">
<div class="card-header">
<div class="card-title">"Kanal"</div>
<p class="card-description">"Ringkasan aktivitas, flagged message, dan budaya kanal."</p>
</div>
<div class="card-content">
<div class="dashboard-list-toolbar">
<input
class="input w-full"
placeholder="Search channels..."
prop:value=search
on:input=move |ev| search_cb.with_value(|cb| cb(event_target_value(&ev)))
/>
</div>
{move || {
if loading && channels.is_empty() {
view! { <ListSkeleton /> }.into_any()
} else if let Some(err) = error.clone() {
view! {
<div class="dashboard-list-empty">
<div class="text-error text-xl">"⚠"</div>
<p class="text-sm text-secondary">{err}</p>
<button class="btn btn-outline btn-sm" on:click=move |_| retry_cb.with_value(|cb| cb())>
"Retry"
</button>
</div>
}.into_any()
} else if channels.is_empty() {
view! {
<div class="dashboard-list-empty">
<div class="text-2xl">"#"</div>
<p class="text-sm text-secondary">"No channels found."</p>
</div>
}.into_any()
} else {
view! {
<div class="dashboard-summary-list">
{channels.clone().into_iter().map(|channel| view! {
<ChannelRow channel=channel />
}).collect::<Vec<_>>()}
</div>
}.into_any()
}
}}
{move || {
(has_more && !loading).then(|| view! {
<div class="mt-4 text-center">
<button class="btn btn-outline btn-sm" on:click=move |_| load_more_cb.with_value(|cb| cb())>
"Load more channels"
</button>
</div>
})
}}
</div>
</div>
}
}
#[component]
fn ChannelRow(channel: DashboardChannel) -> impl IntoView {
let name = channel
.channel_name
.clone()
.unwrap_or_else(|| channel.channel_id.clone());
let summary = channel
.culture_summary
.clone()
.unwrap_or_else(|| format!("{} messages", format_number(channel.total_messages)));
let last_seen = channel.last_message_at.map(format_timestamp);
view! {
<div class="dashboard-summary-row">
<div class="dashboard-summary-avatar dashboard-channel-avatar">
<span>"#"</span>
</div>
<div class="dashboard-summary-main">
<div class="dashboard-summary-title">{format!("#{}", name)}</div>
<div class="dashboard-summary-text">{summary}</div>
<div class="dashboard-summary-meta">
<span>{format!("{} messages", format_number(channel.total_messages))}</span>
<span>{format!("{} flagged", format_number(channel.flagged_count))}</span>
{last_seen.map(|t| view! { <span>{format!("Last: {}", t)}</span> })}
</div>
</div>
</div>
}
}
#[component]
fn ListSkeleton() -> impl IntoView {
view! {
<div class="dashboard-summary-list">
{(0..5).map(|_| view! {
<div class="dashboard-summary-row">
<div class="skeleton skeleton-circular" style="width:40px;height:40px"></div>
<div class="dashboard-summary-main">
<div class="skeleton" style="height:16px;width:160px"></div>
<div class="skeleton mt-2" style="height:14px;width:240px"></div>
</div>
</div>
}).collect::<Vec<_>>()}
</div>
}
}
fn format_number(value: u64) -> String {
let raw = value.to_string();
let mut out = String::new();
for (idx, ch) in raw.chars().rev().enumerate() {
if idx > 0 && idx % 3 == 0 {
out.push(',');
}
out.push(ch);
}
out.chars().rev().collect()
}
fn format_timestamp(ts: i64) -> String {
let d = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64((ts as f64) * 1000.0));
d.to_locale_date_string("en-US", &wasm_bindgen::JsValue::UNDEFINED)
.into()
}
@@ -1,7 +0,0 @@
pub mod channel_summary_list;
pub mod stats_overview;
pub mod user_summary_list;
pub use channel_summary_list::ChannelSummaryList;
pub use stats_overview::StatsOverview;
pub use user_summary_list::UserSummaryList;
@@ -1,150 +0,0 @@
use leptos::prelude::*;
use shared_types::dashboard::{DashboardStats, TopChannel};
#[component]
pub fn StatsOverview(
stats: Option<DashboardStats>,
loading: bool,
error: Option<String>,
on_retry: Box<dyn Fn() + Send + Sync + 'static>,
) -> impl IntoView {
let retry = StoredValue::new(on_retry);
view! {
<div class="dashboard-stats">
{move || {
if loading {
view! { <StatsSkeleton /> }.into_any()
} else if let Some(err) = error.clone() {
view! {
<div class="card p-6 text-center">
<div class="text-error text-2xl mb-2">"⚠"</div>
<p class="text-sm text-secondary mb-4">{err}</p>
<button class="btn btn-outline btn-sm" on:click=move |_| retry.with_value(|cb| cb())>
"Retry"
</button>
</div>
}.into_any()
} else if let Some(stats) = stats.clone() {
view! {
<div class="dashboard-stats-grid">
<MetricCard label="Total Messages" value=stats.total_messages icon="💬" tone="primary" />
<MetricCard label="Today's Messages" value=stats.today_messages icon="📅" tone="success" />
<MetricCard label="Total Users" value=stats.total_users icon="👥" tone="primary" />
<MetricCard label="Active 24h" value=stats.active_users_24h icon="🟢" tone="success" />
<MetricCard label="Flagged" value=stats.total_flagged icon="🚩" tone="error" />
<MetricCard label="Clean" value=stats.total_clean icon="✅" tone="success" />
<MetricCard label="Voice Recordings" value=stats.total_voice_recordings icon="🎙" tone="info" />
<MetricCard label="AI Profiles" value=stats.total_profiles icon="🧠" tone="warning" />
<div class="card dashboard-wide-card">
<div class="card-header">
<div class="card-title">"Top Channels"</div>
</div>
<div class="card-content">
<TopChannels channels=stats.top_channels.clone() />
</div>
</div>
<div class="card dashboard-wide-card">
<div class="card-header">
<div class="card-title">"Moderation Queue"</div>
</div>
<div class="card-content">
<div class="dashboard-moderation-grid">
<QueueMetric label="Pending" value=stats.moderation_overview.pending tone="secondary" />
<QueueMetric label="Processing" value=stats.moderation_overview.processing tone="warning" />
<QueueMetric label="Errors" value=stats.moderation_overview.error tone="error" />
</div>
</div>
</div>
</div>
}.into_any()
} else {
view! {
<div class="card p-6 text-center">
<p class="text-sm text-secondary">"No dashboard data available yet."</p>
</div>
}.into_any()
}
}}
</div>
}
}
#[component]
fn MetricCard(
label: &'static str,
value: u64,
icon: &'static str,
tone: &'static str,
) -> impl IntoView {
view! {
<div class="card dashboard-metric-card">
<div class="dashboard-metric-content">
<div>
<div class="dashboard-metric-label">{label}</div>
<div class="dashboard-metric-value">{format_number(value)}</div>
</div>
<div class=format!("dashboard-metric-icon tone-{}", tone)>{icon}</div>
</div>
</div>
}
}
#[component]
fn QueueMetric(label: &'static str, value: u64, tone: &'static str) -> impl IntoView {
view! {
<div class=format!("dashboard-queue-card tone-{}", tone)>
<div class="dashboard-queue-value">{format_number(value)}</div>
<div class="dashboard-queue-label">{label}</div>
</div>
}
}
#[component]
fn TopChannels(channels: Vec<TopChannel>) -> impl IntoView {
if channels.is_empty() {
return view! { <p class="text-sm text-secondary">"No channel data yet."</p> }.into_any();
}
view! {
<div class="dashboard-top-channels">
{channels.into_iter().map(|ch| {
let name = ch.channel_name.unwrap_or_else(|| ch.channel_id.clone());
view! {
<div class="dashboard-top-channel-row">
<span class="truncate">{format!("#{}", name)}</span>
<span class="font-semibold">{format_number(ch.message_count)}</span>
</div>
}
}).collect::<Vec<_>>()}
</div>
}
.into_any()
}
#[component]
fn StatsSkeleton() -> impl IntoView {
view! {
<div class="dashboard-stats-grid">
{(0..8).map(|_| view! {
<div class="card dashboard-metric-card">
<div class="skeleton" style="height:14px;width:96px"></div>
<div class="skeleton mt-2" style="height:32px;width:72px"></div>
</div>
}).collect::<Vec<_>>()}
</div>
}
}
fn format_number(value: u64) -> String {
let raw = value.to_string();
let mut out = String::new();
for (idx, ch) in raw.chars().rev().enumerate() {
if idx > 0 && idx % 3 == 0 {
out.push(',');
}
out.push(ch);
}
out.chars().rev().collect()
}
@@ -1,148 +0,0 @@
use leptos::prelude::*;
use shared_types::dashboard::DashboardUser;
#[component]
pub fn UserSummaryList(
users: Vec<DashboardUser>,
loading: bool,
error: Option<String>,
search: String,
has_more: bool,
on_search_change: Box<dyn Fn(String) + Send + Sync + 'static>,
on_load_more: Box<dyn Fn() + Send + Sync + 'static>,
on_retry: Box<dyn Fn() + Send + Sync + 'static>,
) -> impl IntoView {
let search_cb = StoredValue::new(on_search_change);
let load_more_cb = StoredValue::new(on_load_more);
let retry_cb = StoredValue::new(on_retry);
view! {
<div class="card dashboard-list-card">
<div class="card-header">
<div class="card-title">"Pengguna"</div>
<p class="card-description">"Ringkasan aktivitas dan trust score pengguna."</p>
</div>
<div class="card-content">
<div class="dashboard-list-toolbar">
<input
class="input w-full"
placeholder="Search users..."
prop:value=search
on:input=move |ev| search_cb.with_value(|cb| cb(event_target_value(&ev)))
/>
</div>
{move || {
if loading && users.is_empty() {
view! { <ListSkeleton /> }.into_any()
} else if let Some(err) = error.clone() {
view! {
<div class="dashboard-list-empty">
<div class="text-error text-xl">"⚠"</div>
<p class="text-sm text-secondary">{err}</p>
<button class="btn btn-outline btn-sm" on:click=move |_| retry_cb.with_value(|cb| cb())>
"Retry"
</button>
</div>
}.into_any()
} else if users.is_empty() {
view! {
<div class="dashboard-list-empty">
<div class="text-2xl">"👤"</div>
<p class="text-sm text-secondary">"No users found."</p>
</div>
}.into_any()
} else {
view! {
<div class="dashboard-summary-list">
{users.clone().into_iter().map(|user| view! {
<UserRow user=user />
}).collect::<Vec<_>>()}
</div>
}.into_any()
}
}}
{move || {
(has_more && !loading).then(|| view! {
<div class="mt-4 text-center">
<button class="btn btn-outline btn-sm" on:click=move |_| load_more_cb.with_value(|cb| cb())>
"Load more users"
</button>
</div>
})
}}
</div>
</div>
}
}
#[component]
fn UserRow(user: DashboardUser) -> impl IntoView {
let name = user
.username
.clone()
.unwrap_or_else(|| user.user_id.clone());
let summary = user
.profile_summary
.clone()
.unwrap_or_else(|| format!("{} messages", format_number(user.total_messages)));
let trust = user.trust_score.map(|score| format!("Trust: {:.2}", score));
let last_seen = user.last_message_at.map(format_timestamp);
view! {
<div class="dashboard-summary-row">
<div class="dashboard-summary-avatar">
{if let Some(url) = user.avatar_url.clone() {
view! { <img src=url alt="" class="dashboard-summary-avatar-img" /> }.into_any()
} else {
view! { <span>"👤"</span> }.into_any()
}}
</div>
<div class="dashboard-summary-main">
<div class="dashboard-summary-title">{name}</div>
<div class="dashboard-summary-text">{summary}</div>
<div class="dashboard-summary-meta">
<span>{format!("{} flagged", format_number(user.flagged_count))}</span>
{trust.map(|t| view! { <span>{t}</span> })}
{last_seen.map(|t| view! { <span>{format!("Last: {}", t)}</span> })}
</div>
</div>
</div>
}
}
#[component]
fn ListSkeleton() -> impl IntoView {
view! {
<div class="dashboard-summary-list">
{(0..5).map(|_| view! {
<div class="dashboard-summary-row">
<div class="skeleton skeleton-circular" style="width:40px;height:40px"></div>
<div class="dashboard-summary-main">
<div class="skeleton" style="height:16px;width:160px"></div>
<div class="skeleton mt-2" style="height:14px;width:240px"></div>
</div>
</div>
}).collect::<Vec<_>>()}
</div>
}
}
fn format_number(value: u64) -> String {
let raw = value.to_string();
let mut out = String::new();
for (idx, ch) in raw.chars().rev().enumerate() {
if idx > 0 && idx % 3 == 0 {
out.push(',');
}
out.push(ch);
}
out.chars().rev().collect()
}
fn format_timestamp(ts: i64) -> String {
let d = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64((ts as f64) * 1000.0));
d.to_locale_date_string("en-US", &wasm_bindgen::JsValue::UNDEFINED)
.into()
}
@@ -1,276 +0,0 @@
pub mod components;
use components::{ChannelSummaryList, StatsOverview, UserSummaryList};
use leptos::prelude::*;
use shared_types::dashboard::{DashboardChannel, DashboardStats, DashboardUser};
use std::sync::Arc;
use wasm_bindgen_futures::spawn_local;
use crate::{log_error, log_info, make_logger};
make_logger!();
#[derive(Clone, PartialEq)]
enum DashboardTab {
Stats,
Users,
Channels,
}
#[component]
pub fn DashboardPanel() -> impl IntoView {
let active_tab = RwSignal::new(DashboardTab::Stats);
let stats = RwSignal::new(None::<DashboardStats>);
let stats_loading = RwSignal::new(false);
let stats_error = RwSignal::new(None::<String>);
let users = RwSignal::new(Vec::<DashboardUser>::new());
let users_loading = RwSignal::new(false);
let users_error = RwSignal::new(None::<String>);
let users_search = RwSignal::new(String::new());
let users_cursor = RwSignal::new(None::<String>);
let channels = RwSignal::new(Vec::<DashboardChannel>::new());
let channels_loading = RwSignal::new(false);
let channels_error = RwSignal::new(None::<String>);
let channels_search = RwSignal::new(String::new());
let channels_cursor = RwSignal::new(None::<String>);
let fetch_stats: Arc<dyn Fn() + Send + Sync + 'static> = Arc::new(move || {
stats_loading.set(true);
stats_error.set(None);
log_info!("Dashboard fetching stats...");
spawn_local(async move {
match crate::api::dashboard::get_dashboard_stats().await {
Ok(data) => {
log_info!("Dashboard stats loaded: {} messages", data.total_messages);
stats.set(Some(data));
}
Err(err) => {
log_error!("Dashboard stats error: {}", err);
stats_error.set(Some(format!("Failed to load stats: {}", err)));
}
}
stats_loading.set(false);
});
});
let fetch_users: Arc<dyn Fn(bool) + Send + Sync + 'static> = Arc::new(move |reset: bool| {
if users_loading.get() {
return;
}
users_loading.set(true);
users_error.set(None);
log_info!("Dashboard fetching users...");
let cursor = if reset { None } else { users_cursor.get() };
let search = users_search.get();
spawn_local(async move {
let search_ref = (!search.trim().is_empty()).then_some(search.trim());
match crate::api::dashboard::get_dashboard_users(
Some(20),
cursor.as_deref(),
search_ref,
)
.await
{
Ok(page) => {
log_info!("Dashboard users loaded: {} users", page.data.len());
if reset {
users.set(page.data);
} else {
let mut current = users.get();
current.extend(page.data);
users.set(current);
}
users_cursor.set(page.next_cursor);
}
Err(err) => {
log_error!("Dashboard users error: {}", err);
users_error.set(Some(format!("Failed to load users: {}", err)));
}
}
users_loading.set(false);
});
});
let fetch_channels: Arc<dyn Fn(bool) + Send + Sync + 'static> = Arc::new(move |reset: bool| {
if channels_loading.get() {
return;
}
channels_loading.set(true);
channels_error.set(None);
log_info!("Dashboard fetching channels...");
let cursor = if reset { None } else { channels_cursor.get() };
let search = channels_search.get();
spawn_local(async move {
let search_ref = (!search.trim().is_empty()).then_some(search.trim());
match crate::api::dashboard::get_dashboard_channels(
Some(20),
cursor.as_deref(),
search_ref,
None,
)
.await
{
Ok(page) => {
log_info!("Dashboard channels loaded: {} channels", page.data.len());
if reset {
channels.set(page.data);
} else {
let mut current = channels.get();
current.extend(page.data);
channels.set(current);
}
channels_cursor.set(page.next_cursor);
}
Err(err) => {
log_error!("Dashboard channels error: {}", err);
channels_error.set(Some(format!("Failed to load channels: {}", err)));
}
}
channels_loading.set(false);
});
});
// Initial fetch on mount (use spawn_local to avoid reactive dependency tracking)
{
let fetch_stats = fetch_stats.clone();
let fetch_users = fetch_users.clone();
let fetch_channels = fetch_channels.clone();
spawn_local(async move {
fetch_stats();
fetch_users(true);
fetch_channels(true);
});
}
view! {
<div class="dashboard-panel">
<div class="dashboard-header">
<div>
<h2 class="text-2xl font-bold">"Dashboard Guild"</h2>
<p class="text-sm text-secondary mt-2">
"Pantau statistik, profil pengguna, dan aktivitas kanal komunitas IMPHNEN."
</p>
</div>
</div>
<div class="tabs">
<div class="tab-list mb-6">
<DashboardTabButton tab=DashboardTab::Stats active_tab=active_tab label="Statistik" icon="📊" />
<DashboardTabButton tab=DashboardTab::Users active_tab=active_tab label="Pengguna" icon="👥" />
<DashboardTabButton tab=DashboardTab::Channels active_tab=active_tab label="Kanal" icon="#" />
</div>
<div class="tab-content" style:display=move || if active_tab.get() == DashboardTab::Stats { "block" } else { "none" }>
{move || {
let on_retry = {
let fetch_stats = fetch_stats.clone();
Box::new(move || fetch_stats())
};
view! {
<StatsOverview
stats=stats.get()
loading=stats_loading.get()
error=stats_error.get()
on_retry=on_retry
/>
}
}}
</div>
<div class="tab-content" style:display=move || if active_tab.get() == DashboardTab::Users { "block" } else { "none" }>
{move || {
let on_search_change = {
let fetch_users = fetch_users.clone();
Box::new(move |value| {
users_search.set(value);
users_cursor.set(None);
fetch_users(true);
})
};
let on_load_more = {
let fetch_users = fetch_users.clone();
Box::new(move || fetch_users(false))
};
let on_retry = {
let fetch_users = fetch_users.clone();
Box::new(move || fetch_users(true))
};
view! {
<UserSummaryList
users=users.get()
loading=users_loading.get()
error=users_error.get()
search=users_search.get()
has_more=users_cursor.get().is_some()
on_search_change=on_search_change
on_load_more=on_load_more
on_retry=on_retry
/>
}
}}
</div>
<div class="tab-content" style:display=move || if active_tab.get() == DashboardTab::Channels { "block" } else { "none" }>
{move || {
let on_search_change = {
let fetch_channels = fetch_channels.clone();
Box::new(move |value| {
channels_search.set(value);
channels_cursor.set(None);
fetch_channels(true);
})
};
let on_load_more = {
let fetch_channels = fetch_channels.clone();
Box::new(move || fetch_channels(false))
};
let on_retry = {
let fetch_channels = fetch_channels.clone();
Box::new(move || fetch_channels(true))
};
view! {
<ChannelSummaryList
channels=channels.get()
loading=channels_loading.get()
error=channels_error.get()
search=channels_search.get()
has_more=channels_cursor.get().is_some()
on_search_change=on_search_change
on_load_more=on_load_more
on_retry=on_retry
/>
}
}}
</div>
</div>
</div>
}
}
#[component]
fn DashboardTabButton(
tab: DashboardTab,
active_tab: RwSignal<DashboardTab>,
label: &'static str,
icon: &'static str,
) -> impl IntoView {
let tab_for_class = tab.clone();
let tab_for_aria = tab.clone();
let tab_for_click = tab;
view! {
<button
class="tab-trigger"
class:active=move || active_tab.get() == tab_for_class
aria-selected=move || if active_tab.get() == tab_for_aria { "true" } else { "false" }
on:click=move |_| active_tab.set(tab_for_click.clone())
>
<span>{icon}</span>
<span>{label}</span>
</button>
}
}
@@ -1,2 +0,0 @@
pub mod pcm_decoder;
pub mod ring_buffer;
@@ -1,70 +0,0 @@
use wasm_bindgen::prelude::*;
/// PCM Frame decoded from binary WebSocket data
/// Format: [u32 userId (4 bytes)][i16 samples (N bytes)]
pub struct PcmFrame {
pub user_id: u32,
pub samples: Vec<f32>, // Normalized to [-1.0, 1.0]
}
/// Decode a binary WebSocket message into PCM frames
/// Returns None if data is too short or malformed
pub fn decode_pcm_frame(data: &[u8]) -> Option<PcmFrame> {
if data.len() < 4 {
return None;
}
let user_id = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
let sample_bytes = &data[4..];
let sample_count = sample_bytes.len() / 2;
if sample_count == 0 {
return None;
}
let samples = decode_i16_samples(sample_bytes);
Some(PcmFrame { user_id, samples })
}
/// Decode raw i16 PCM bytes to normalized f32 samples [-1.0, 1.0]
pub fn decode_i16_samples(data: &[u8]) -> Vec<f32> {
let count = data.len() / 2;
let mut out = Vec::with_capacity(count);
for i in 0..count {
let offset = i * 2;
if offset + 1 < data.len() {
let sample = i16::from_le_bytes([data[offset], data[offset + 1]]);
out.push((sample as f32) / 32768.0);
}
}
out
}
/// Encode f32 samples [-1.0, 1.0] to base64 for WebSocket transmission
/// Uses JavaScript btoa for encoding
pub fn encode_samples_to_base64(samples: &[f32]) -> String {
// Convert f32 samples to i16 bytes
let mut bytes = Vec::with_capacity(samples.len() * 2);
for &sample in samples {
let clamped = sample.clamp(-1.0, 1.0);
let int_sample = (clamped * 32767.0) as i16;
bytes.extend_from_slice(&int_sample.to_le_bytes());
}
encode_bytes_base64(&bytes)
}
/// Encode raw bytes to base64 using JavaScript's btoa via wasm-bindgen
fn encode_bytes_base64(data: &[u8]) -> String {
// Convert bytes 0-255 to a Latin-1 string (each byte → char with same codepoint)
let latin1: String = data.iter().map(|&b| b as char).collect();
js_btoa(&latin1)
}
/// Direct wasm-bindgen binding to the browser's btoa function
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = btoa)]
fn js_btoa(input: &str) -> String;
}
@@ -1,124 +0,0 @@
use std::sync::{Arc, Mutex};
/// AudioRingBuffer — Fixed-size circular buffer for real-time PCM streaming
/// Provides thread-safe write/read with automatic overwrite protection
pub struct AudioRingBuffer {
buffer: Vec<f32>,
capacity: usize,
write_pos: usize,
read_pos: usize,
available: usize,
}
impl AudioRingBuffer {
/// Create a new ring buffer with given capacity (in samples)
pub fn new(capacity: usize) -> Self {
Self {
buffer: vec![0.0; capacity],
capacity,
write_pos: 0,
read_pos: 0,
available: 0,
}
}
/// Write samples to the ring buffer. Overwrites oldest data if full.
pub fn write(&mut self, samples: &[f32]) {
let mut written = 0;
while written < samples.len() {
let chunk = (samples.len() - written).min(self.capacity - self.write_pos);
let src = &samples[written..written + chunk];
let dest = &mut self.buffer[self.write_pos..self.write_pos + chunk];
dest.copy_from_slice(src);
written += chunk;
self.write_pos = (self.write_pos + chunk) % self.capacity;
self.available = (self.available + chunk).min(self.capacity);
// If we overwrote unread data, advance read_pos
if self.available == self.capacity {
self.read_pos = self.write_pos;
}
}
}
/// Read up to `max_samples` from the buffer. Returns the samples read.
pub fn read(&mut self, max_samples: usize) -> Vec<f32> {
let to_read = max_samples.min(self.available);
let mut out = Vec::with_capacity(to_read);
let mut remaining = to_read;
while remaining > 0 {
let chunk = remaining.min(self.capacity - self.read_pos);
out.extend_from_slice(&self.buffer[self.read_pos..self.read_pos + chunk]);
remaining -= chunk;
self.read_pos = (self.read_pos + chunk) % self.capacity;
}
self.available -= to_read;
out
}
/// Number of samples available to read
pub fn available_samples(&self) -> usize {
self.available
}
/// Clear all buffered data
pub fn clear(&mut self) {
self.write_pos = 0;
self.read_pos = 0;
self.available = 0;
}
}
/// Thread-safe wrapper around AudioRingBuffer
pub struct SharedRingBuffer {
inner: Arc<Mutex<AudioRingBuffer>>,
}
impl SharedRingBuffer {
pub fn new(capacity: usize) -> Self {
Self {
inner: Arc::new(Mutex::new(AudioRingBuffer::new(capacity))),
}
}
pub fn write(&self, samples: &[f32]) {
if let Ok(mut guard) = self.inner.lock() {
guard.write(samples);
}
}
pub fn read(&self, max_samples: usize) -> Vec<f32> {
if let Ok(mut guard) = self.inner.lock() {
guard.read(max_samples)
} else {
Vec::new()
}
}
pub fn available_samples(&self) -> usize {
if let Ok(guard) = self.inner.lock() {
guard.available_samples()
} else {
0
}
}
pub fn clear(&self) {
if let Ok(mut guard) = self.inner.lock() {
guard.clear();
}
}
pub fn clone_inner(&self) -> Arc<Mutex<AudioRingBuffer>> {
self.inner.clone()
}
}
impl Clone for SharedRingBuffer {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
@@ -1,80 +0,0 @@
use leptos::prelude::*;
use shared_types::voice::ActiveSpeaker;
/// ActiveSpeakers component for Leptos
/// Displays a real-time list of speaking users with avatar and status indicator
#[component]
pub fn ActiveSpeakers(
#[prop(optional)] speakers: RwSignal<Vec<ActiveSpeaker>>,
#[prop(optional)] class: &'static str,
) -> impl IntoView {
let empty_state = move || speakers.get().is_empty();
view! {
<div class=class>
<Show
when=empty_state
fallback=move || {
view! {
<div class="speak-list">
<For
each=move || speakers.get()
key=|s| s.user_id.clone() + &s.username
let:speaker
>
<div style="display:flex;align-items:center;gap:0.75rem;border-radius:0.75rem;border:1px solid var(--surface-border);background:var(--surface-base);padding:0.75rem">
<div style="width:2rem;height:2rem;flex-shrink:0">
{speaker.avatar.as_ref().map(|avatar_url| {
let url = avatar_url.clone();
view! {
<img
src=url
alt=""
style="width:2rem;height:2rem;border-radius:9999px;object-fit:cover;box-shadow:0 0 0 2px rgba(35,161,235,0.3)"
/>
}
})}
</div>
<div style="min-width:0;flex:1">
<div class="truncate text-sm font-medium">
{speaker.username.clone()}
</div>
<div style="display:flex;align-items:center;gap:0.375rem">
<span style=move || {
if speaker.speaking {
"display:inline-block;width:0.5rem;height:0.5rem;border-radius:9999px;background:#10b981"
} else {
"display:inline-block;width:0.5rem;height:0.5rem;border-radius:9999px;background:color-mix(in srgb, var(--text-tertiary) 40%, transparent)"
}
}></span>
<span style=move || {
if speaker.speaking {
"font-size:0.75rem;font-weight:500;color:#059669"
} else {
"font-size:0.75rem;font-weight:500;color:var(--text-secondary)"
}
}>
{move || if speaker.speaking { "Speaking" } else { "Silent" }}
</span>
</div>
</div>
</div>
</For>
</div>
}
}
>
<div style="border-radius:0.75rem;border:1px solid var(--surface-border);background:var(--surface-base);padding:2rem;text-align:center">
<div>
<div style="font-size:2.25rem;line-height:2.5rem">
"🎤"
</div>
<p style="font-size:0.875rem;color:var(--text-secondary)">
"No active speakers"
</p>
</div>
</div>
</Show>
</div>
}
}
@@ -1,79 +0,0 @@
use leptos::prelude::*;
use std::sync::{Arc, Mutex};
/// AudioVisualizer — Real-time 32-bar frequency spectrum display
/// Simplified implementation using CSS bars updated via signals
#[component]
pub fn AudioVisualizer(
#[prop(default = true)] _active: bool,
#[prop(optional)] pcm_data: Option<Arc<Mutex<Vec<f32>>>>,
) -> impl IntoView {
let bars = RwSignal::new(vec![0.0; 32]);
let (tick, set_tick) = signal(0u32);
// Drive periodic updates: increment tick every 100ms
wasm_bindgen_futures::spawn_local(async move {
loop {
gloo_timers::future::TimeoutFuture::new(100).await;
set_tick.update(|t| *t = t.wrapping_add(1));
}
});
// Effect reacts to tick changes, updating bars from PCM data each frame
Effect::new(move |_| {
tick.get(); // Track — Effect re-runs on each tick (every 100ms)
if let Some(ref pcm_arc) = pcm_data {
if let Ok(pcm_vec) = pcm_arc.lock() {
let computed = compute_frequency_bands(&pcm_vec);
bars.update(|b| {
for (i, band) in b.iter_mut().enumerate() {
let target = computed.get(i).copied().unwrap_or(0.0).clamp(0.0, 1.0);
*band = *band * 0.7 + target * 0.3; // Smooth decay
}
});
}
}
});
view! {
<div class="audio-visualizer">
<div class="audio-visualizer-bars">
{(0..32).map(|i| {
view! {
<div
class="audio-bar"
style=move || {
let height = bars.get()[i] * 100.0;
format!("height: {}%", height)
}
></div>
}
}).collect::<Vec<_>>()}
</div>
</div>
}
}
/// Compute 32-band frequency spectrum from PCM samples
fn compute_frequency_bands(pcm_samples: &[f32]) -> Vec<f32> {
let mut bands = vec![0.0; 32];
if pcm_samples.is_empty() {
return bands;
}
let samples_per_band = (pcm_samples.len() / 32).max(1);
for (band_idx, band) in bands.iter_mut().enumerate() {
let start = band_idx * samples_per_band;
let end = ((band_idx + 1) * samples_per_band).min(pcm_samples.len());
if start < pcm_samples.len() {
let slice = &pcm_samples[start..end];
let rms = (slice.iter().map(|s| s * s).sum::<f32>() / slice.len() as f32).sqrt();
*band = rms.min(1.0);
}
}
bands
}
@@ -1,94 +0,0 @@
use leptos::prelude::*;
use std::sync::{Arc, Mutex};
/// MicLevelMeter — Horizontal level indicator for microphone input
/// Displays 0-100% amplitude as a filling bar with smooth decay
#[component]
pub fn MicLevelMeter(
#[prop(default = true)] active: bool,
#[prop(optional)] pcm_data: Option<Arc<Mutex<Vec<f32>>>>,
#[prop(optional)] label: Option<&'static str>,
) -> impl IntoView {
let level = RwSignal::new(0.0f32);
let peak = RwSignal::new(0.0f32);
let (tick, set_tick) = signal(0u32);
// Drive periodic updates: increment tick every 100ms
wasm_bindgen_futures::spawn_local(async move {
loop {
gloo_timers::future::TimeoutFuture::new(100).await;
set_tick.update(|t| *t = t.wrapping_add(1));
}
});
// Effect reacts to tick changes, updating level from PCM data each frame
Effect::new(move |_| {
tick.get(); // Track — Effect re-runs on each tick
if !active {
return;
}
if let Some(ref pcm_arc) = pcm_data {
if let Ok(pcm_vec) = pcm_arc.lock() {
let current_level = compute_rms(&pcm_vec);
level.update(|l| {
*l = *l * 0.8 + current_level * 0.2; // Smooth decay
});
peak.update(|p| {
*p = (*p * 0.95).max(current_level); // Peak hold with decay
});
}
}
});
let level_percent = move || (level.get() * 100.0).min(100.0);
let peak_percent = move || (peak.get() * 100.0).min(100.0);
// Determine color based on level
let level_color = move || {
let l = level.get();
if l < 0.5 {
"bg-green-500"
} else if l < 0.75 {
"bg-yellow-500"
} else {
"bg-red-500"
}
};
view! {
<div class="mic-level-meter">
{label.map(|l| view! {
<label style="font-size:0.75rem;font-weight:500;color:var(--text-secondary);display:block;margin-bottom:var(--space-1)">{l}</label>
})}
<div class="mic-row">
{/* Main level bar */}
<div class="mic-track">
<div
class=move || format!("h-full {} transition-all", level_color())
style=move || format!("width: {}%", level_percent())
></div>
{/* Peak indicator */}
<div
class="mic-clip"
style=move || format!("left: {}%", peak_percent())
></div>
</div>
{/* Percentage display */}
<span style="font-size:0.75rem;font-family:monospace;width:2rem;text-align:right">
{move || format!("{}%", (level_percent() as u8))}
</span>
</div>
</div>
}
}
/// Compute RMS (Root Mean Square) amplitude from PCM samples
/// Returns normalized value 0.0-1.0
fn compute_rms(samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let mean_square = samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32;
mean_square.sqrt()
}
@@ -1,19 +0,0 @@
pub mod active_speakers;
pub mod audio_visualizer;
pub mod mic_level_meter;
pub mod music_sub_panel;
pub mod now_playing;
pub mod recordings_sub_panel;
pub mod screen_sub_panel;
pub mod voice_connection_card;
pub mod waveform_player;
pub use active_speakers::ActiveSpeakers;
pub use audio_visualizer::AudioVisualizer;
pub use mic_level_meter::MicLevelMeter;
pub use music_sub_panel::MusicSubPanel;
pub use now_playing::NowPlaying;
pub use recordings_sub_panel::RecordingsSubPanel;
pub use screen_sub_panel::ScreenSubPanel;
pub use voice_connection_card::VoiceConnectionCard;
pub use waveform_player::WaveformPlayer;
@@ -1,53 +0,0 @@
use leptos::prelude::*;
/// MusicSubPanel — Music playlist controls and URL input
#[component]
pub fn MusicSubPanel(
#[prop(optional)] on_queue: Option<Box<dyn Fn(String) + Send + Sync + 'static>>,
) -> impl IntoView {
let (url_input, set_url_input) = signal::<String>(String::new());
let handle_queue_click = move |_| {
let url = url_input.get_untracked().trim().to_string();
if !url.is_empty() {
if let Some(ref cb) = on_queue {
cb(url.clone());
set_url_input.set(String::new());
}
}
};
view! {
<div class="card">
<div class="card-header">
<div class="card-title" style="display:flex;align-items:center;gap:var(--space-2)">
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9 8h6v8h-6z"></path>
</svg>
"Music"
</div>
</div>
<div class="card-content music-body">
<div class="music-body">
<label style="font-size:0.75rem;font-weight:500;color:var(--text-secondary)">"YouTube URL or Search"</label>
<input
type="text"
class="input"
placeholder="youtube.com/watch?v=... or song name"
prop:value=url_input
on:input=move |ev| set_url_input.set(event_target_value(&ev))
/>
</div>
<button
class="btn btn-primary"
style="width:100%"
on:click=handle_queue_click
disabled=move || url_input.get().is_empty()
>
"Queue Music"
</button>
</div>
</div>
}
}
@@ -1,106 +0,0 @@
use leptos::prelude::*;
use shared_types::media::MediaState;
/// NowPlaying — Displays current media item and queue info
/// Accepts an optional RwSignal to enable real-time updates from WebSocket events.
#[component]
pub fn NowPlaying(
#[prop(optional)] media_rw: Option<RwSignal<Option<MediaState>>>,
#[prop(optional)] on_skip: Option<Box<dyn Fn() + Send + Sync + 'static>>,
#[prop(optional)] on_stop: Option<Box<dyn Fn() + Send + Sync + 'static>>,
) -> impl IntoView {
// Use provided signal, or fall back to a local one for static usage
let media_state = media_rw.unwrap_or_else(|| RwSignal::new(None));
// Wrap callbacks in StoredValue for shareable non-Clone ownership in Leptos context
let skip_cb = StoredValue::new(on_skip);
let stop_cb = StoredValue::new(on_stop);
let has_skip = skip_cb.with_value(|v| v.is_some());
let has_stop = stop_cb.with_value(|v| v.is_some());
view! {
<div class="card">
<div class="card-header">
<div class="card-title">"Now Playing"</div>
</div>
<div class="card-content np-body">
{move || {
media_state.get().map(|ms| {
let current = ms.current.as_ref().cloned();
let queue_len = ms.queue.len();
view! {
<>
{current.map(|item| {
let title = item.title.clone().unwrap_or_else(|| "Unknown".to_string());
let duration_ms = item.duration_ms.unwrap_or(0);
let duration_sec = duration_ms / 1000;
view! {
<div class="np-body">
<div class="text-sm font-medium text-foreground truncate">
{title}
</div>
<div class="np-meta">
<span>{format!("{}s", duration_sec)}</span>
</div>
<div class="np-tags">
{has_skip.then(|| {
view! {
<button
class="btn btn-sm btn-outline flex-1"
on:click=move |_| { skip_cb.with_value(|cb| { if let Some(cb) = cb { cb(); } }); }
>
"⏭ Skip"
</button>
}
})}
{has_stop.then(|| {
view! {
<button
class="btn btn-sm btn-destructive flex-1"
on:click=move |_| { stop_cb.with_value(|cb| { if let Some(cb) = cb { cb(); } }); }
>
"⏹ Stop"
</button>
}
})}
</div>
</div>
}
})}
{(queue_len > 0).then(|| {
view! {
<div class="border-t border-border/50 pt-3">
<div class="text-xs font-medium text-muted-foreground">
{format!("Queue: {} item{}", queue_len, if queue_len == 1 { "" } else { "s" })}
</div>
</div>
}
})}
{(queue_len == 0).then(|| {
view! {
<div class="text-xs text-muted-foreground text-center py-2">
"Queue is empty"
</div>
}
})}
</>
}
})
}}
{move || {
media_state.get().is_none().then(|| {
view! {
<div class="text-xs text-muted-foreground text-center py-4">
"No media connected"
</div>
}
})
}}
</div>
</div>
}
}
@@ -1,181 +0,0 @@
use crate::api::recordings::{delete_recording, get_recordings};
use leptos::prelude::*;
use shared_types::recording::VoiceRecording;
/// RecordingsSubPanel — Paginated list of voice recordings
/// Accepts an optional refresh_trigger signal to reload when a new recording is uploaded.
#[component]
pub fn RecordingsSubPanel(
#[prop(optional)] refresh_trigger: Option<ReadSignal<u64>>,
) -> impl IntoView {
let recordings = RwSignal::new(Vec::<VoiceRecording>::new());
let loading = RwSignal::new(false);
let has_more = RwSignal::new(true);
let next_cursor = RwSignal::new(None::<String>);
// Load recordings
let load = move |reset: bool| {
if loading.get_untracked() {
return;
}
loading.set(true);
let cursor_val = if reset {
None
} else {
next_cursor.get_untracked()
};
wasm_bindgen_futures::spawn_local({
async move {
match get_recordings(Some(20), cursor_val.as_deref()).await {
Ok(resp) => {
if reset {
recordings.set(resp.items);
} else {
let mut current = recordings.get_untracked();
current.extend(resp.items);
recordings.set(current);
}
has_more.set(resp.has_more);
next_cursor.set(resp.next_cursor);
}
Err(_) => {
if reset {
recordings.set(Vec::new());
}
}
}
loading.set(false);
}
});
};
// Load on mount, and reload when refresh_trigger changes (e.g., new recording uploaded)
Effect::new(move |_| {
if let Some(trigger) = refresh_trigger {
trigger.get(); // Track — re-run when WS signals a new recording
}
load(true);
});
// Delete recording handler
let do_delete = move |id: String| {
wasm_bindgen_futures::spawn_local({
let id = id.clone();
async move {
let _ = delete_recording(&id).await;
recordings.update(|r| r.retain(|rec| rec.id != id));
}
});
};
view! {
<div class="recordings-sub-panel card">
<div class="card-header">
<div class="card-title flex items-center gap-2">
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
"Recordings"
</div>
<p class="card-description">"Voice channel recordings from all sessions."</p>
</div>
<div class="card-content">
{move || {
let recs = recordings.get();
if recs.is_empty() && !loading.get() {
view! {
<div class="rec-empty">
<p style="font-size:0.875rem;color:var(--text-secondary)">"No recordings yet."</p>
<p style="font-size:0.75rem;color:var(--text-secondary)">"Join a voice channel to start recording."</p>
</div>
}.into_any()
} else {
view! {
<div class="rec-list">
{recs.iter().map(|rec| {
let id = rec.id.clone();
let username = rec.username.clone();
let channel_name = rec.channel_name.clone().unwrap_or_default();
let created_at = format_timestamp(rec.created_at);
let has_url = rec.download_url.is_some();
let url = rec.download_url.clone().unwrap_or_default();
view! {
<div class="rec-item">
<div class="rec-info">
<div class="rec-name">{username}</div>
<div class="rec-meta">
<span>{channel_name}</span>
<span>"·"</span>
<span>{format_size(rec.size_bytes)}</span>
<span>"·"</span>
<span>{created_at}</span>
</div>
</div>
<div class="rec-actions">
{has_url.then(|| {
view! {
<a
href=url
target="_blank"
class="btn btn-sm btn-outline"
>
"Download"
</a>
}
})}
<button
class="btn btn-sm btn-ghost text-destructive hover:text-destructive"
on:click=move |_| do_delete(id.clone())
>
"🗑"
</button>
</div>
</div>
}
}).collect::<Vec<_>>()}
</div>
}.into_any()
}
}}
{move || {
(has_more.get() && !loading.get()).then(|| {
view! {
<div class="rec-footer">
<button
class="btn btn-sm btn-outline"
on:click=move |_| load(false)
>
"Load more"
</button>
</div>
}
})
}}
</div>
</div>
}
}
/// Format file size bytes to human readable
fn format_size(bytes: u64) -> String {
if bytes < 1024 {
format!("{} B", bytes)
} else if bytes < 1024 * 1024 {
format!("{:.1} KB", bytes as f64 / 1024.0)
} else {
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
}
}
/// Format timestamp i64 to readable date
fn format_timestamp(ts: i64) -> String {
let d = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64((ts as f64) * 1000.0));
d.to_locale_date_string("en-US", &wasm_bindgen::JsValue::UNDEFINED)
.into()
}
@@ -1,83 +0,0 @@
use leptos::prelude::*;
/// ScreenSubPanel — Screenshare controls
#[component]
pub fn ScreenSubPanel(
#[prop(optional)] on_start_stream: Option<Box<dyn Fn() + Send + Sync + 'static>>,
#[prop(optional)] on_stop_stream: Option<Box<dyn Fn() + Send + Sync + 'static>>,
) -> impl IntoView {
let (is_streaming, set_is_streaming) = signal::<bool>(false);
let has_start = on_start_stream.is_some();
let has_stop = on_stop_stream.is_some();
view! {
<div class="card">
<div class="card-header">
<div class="card-title" style="display:flex;align-items:center;gap:var(--space-2)">
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
"Screenshare"
</div>
</div>
<div class="card-content" style="display:flex;flex-direction:column;gap:var(--space-3)">
<p style="font-size:0.75rem;color:var(--text-secondary)">
"Stream your screen to the voice channel for everyone to see."
</p>
<div class="scrn-actions">
{has_start.then(|| {
view! {
<button
class=move || format!("btn btn-success flex-1 {}", if is_streaming.get() { "opacity-50" } else { "" })
disabled=move || is_streaming.get()
on:click=move |_| {
if !is_streaming.get_untracked() {
set_is_streaming.set(true);
if let Some(ref cb) = on_start_stream {
cb();
}
}
}
>
"🔴 Start Stream"
</button>
}
})}
{has_stop.then(|| {
view! {
<button
class=move || format!("btn btn-destructive flex-1 {}", if !is_streaming.get() { "opacity-50" } else { "" })
disabled=move || !is_streaming.get()
on:click=move |_| {
if is_streaming.get_untracked() {
set_is_streaming.set(false);
if let Some(ref cb) = on_stop_stream {
cb();
}
}
}
>
"⏹ Stop Stream"
</button>
}
})}
</div>
{move || {
is_streaming.get().then(|| {
view! {
<div class="scrn-status">
"🔴 Live streaming..."
</div>
}
})
}}
</div>
</div>
}
}
@@ -1,220 +0,0 @@
use crate::features::live::hooks::use_voice_control::{use_voice_control, VoiceControlState};
use leptos::prelude::*;
use wasm_bindgen::JsCast;
/// VoiceConnectionCard component for Leptos
/// Renders guild and voice channel selectors with connect/disconnect controls
#[component]
pub fn VoiceConnectionCard(
#[prop(optional)] voice_state: Option<VoiceControlState>,
#[prop(optional)] class: &'static str,
) -> impl IntoView {
let default_state = use_voice_control();
let state = voice_state.unwrap_or(default_state);
// Reactive signal for selected guild
let (selected_guild, set_selected_guild) = signal::<String>(String::new());
// Reactive signal for selected channel
let (selected_channel, set_selected_channel) = signal::<String>(String::new());
// When guild is selected, load voice channels
Effect::new(move |_| {
let guild_id = selected_guild.get();
if !guild_id.is_empty() {
(state.load_voice_channels)(guild_id);
}
});
// Load guilds on mount
Effect::new(move |_| {
(state.load_guilds)();
});
let on_guild_change = move |ev: leptos::ev::Event| {
if let Some(target) = ev.target() {
if let Ok(select_el) = target.dyn_into::<web_sys::HtmlSelectElement>() {
set_selected_guild.set(select_el.value());
}
}
};
let on_channel_change = move |ev: leptos::ev::Event| {
if let Some(target) = ev.target() {
if let Ok(select_el) = target.dyn_into::<web_sys::HtmlSelectElement>() {
set_selected_channel.set(select_el.value());
}
}
};
let on_join_click = move |_| {
let guild_id = selected_guild.get();
let channel_id = selected_channel.get();
if !guild_id.is_empty() && !channel_id.is_empty() {
(state.join_voice)(guild_id, channel_id);
}
};
let on_disconnect_click = move |_| {
(state.leave_voice)();
};
// Read signals for reactive rendering
let guilds = state.guilds;
let voice_channels = state.voice_channels;
let loading = state.loading;
let error = state.error;
let voice_status = state.voice_status;
let is_connected = move || voice_status.get().map(|s| s.connected).unwrap_or(false);
let can_join = move || {
!selected_guild.get().is_empty() && !selected_channel.get().is_empty() && !loading.get()
};
let can_disconnect = move || is_connected() && !loading.get();
view! {
<div class=format!("rounded-xl border border-border bg-card shadow-sm {}", class)>
<div class="p-6">
<div class="flex items-center gap-2 mb-2">
<svg
class="h-5 w-5 text-primary"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z" />
</svg>
<h3 class="text-lg font-semibold tracking-tight">"Voice Bridge"</h3>
</div>
<p class="text-sm text-muted-foreground mb-4">
"Join a Discord voice channel, listen, and transmit audio."
</p>
{/* Guild and Channel Selectors */}
<div class="grid gap-4 md:grid-cols-2 mb-4">
<div class="space-y-2">
<label class="text-sm font-medium text-foreground">"Guild"</label>
<select
prop:value=selected_guild
on:change=on_guild_change
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">"Select guild"</option>
<For each=move || guilds.get() key=|g| g.id.clone() let:guild>
<option value=guild.id.clone()>
{guild.name.clone()}
</option>
</For>
</select>
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-foreground">"Voice Channel"</label>
<select
prop:value=selected_channel
on:change=on_channel_change
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">"Select voice channel"</option>
<For each=move || voice_channels.get() key=|c| c.id.clone() let:channel>
<option value=channel.id.clone()>
{channel.name.clone()}
</option>
</For>
</select>
</div>
</div>
{/* Error Display */}
{move || {
error.get().map(|err| {
view! {
<div class="rounded-md bg-destructive/15 px-3 py-2 text-sm text-destructive mb-4">
{err}
</div>
}
})
}}
{/* Status Display */}
{move || {
voice_status.get().map(|status| {
let connected = status.connected;
let active_channel = status.active_channel_name.clone();
view! {
<div class="flex items-center gap-2 text-sm mb-4">
<div class=move || {
if connected {
"h-2 w-2 rounded-full bg-emerald-500"
} else {
"h-2 w-2 rounded-full bg-muted-foreground/40"
}
}></div>
<span class=move || {
if connected {
"text-emerald-600 dark:text-emerald-400 font-medium"
} else {
"text-muted-foreground"
}
}>
{if connected { "Connected" } else { "Disconnected" }}
</span>
{active_channel.map(|name| {
view! {
<span class="text-muted-foreground">
{format!(" - {}", name)}
</span>
}
})}
</div>
}
})
}}
{/* Control Buttons */}
<div class="flex flex-wrap gap-2">
<button
class=move || {
if can_join() {
"btn btn-primary"
} else {
"btn btn-primary opacity-50 cursor-not-allowed"
}
}
disabled=move || !can_join()
on:click=on_join_click
>
{move || if is_connected() { "Reconnect" } else { "Join Voice" }}
</button>
<button
class=move || {
if can_disconnect() {
"btn btn-destructive"
} else {
"btn btn-destructive opacity-50 cursor-not-allowed"
}
}
disabled=move || !can_disconnect()
on:click=on_disconnect_click
>
"Disconnect"
</button>
{move || {
if loading.get() {
view! {
<span class="inline-flex items-center px-3 py-2 text-sm text-muted-foreground">
"Loading..."
</span>
}.into_any()
} else {
let _: () = view! { <></> };
().into_any()
}
}}
</div>
</div>
</div>
}
}
@@ -1,103 +0,0 @@
use leptos::prelude::*;
use wasm_bindgen::JsCast;
/// WaveformPlayer — Audio player with waveform progress bar
#[component]
pub fn WaveformPlayer(
audio_url: String,
#[prop(default = "Recording".to_string())] title: String,
) -> impl IntoView {
let is_playing = RwSignal::new(false);
let current_time = RwSignal::new(0.0);
let duration = RwSignal::new(0.0);
let audio_id = format!("audio_{}", audio_url);
// Clone audio_url for the audio element
let audio_src = audio_url.clone();
let audio_src_for_id = audio_src.clone();
let toggle_play = move |_| {
let doc = web_sys::window().unwrap().document().unwrap();
let audio_opt = doc.get_element_by_id(&format!("audio_{}", audio_src_for_id));
if let Some(audio_el) = audio_opt {
if let Ok(audio) = audio_el.dyn_into::<web_sys::HtmlAudioElement>() {
if is_playing.get_untracked() {
let _ = audio.pause();
is_playing.set(false);
} else {
if audio.ended() {
audio.set_current_time(0.0);
}
if audio.play().is_ok() {
is_playing.set(true);
}
}
}
}
};
let _ = audio_url; // Mark as used for the audio_id
view! {
<div class="wave-wrap">
<audio
id=audio_id.clone()
preload="auto"
src=audio_src
style="display:none"
on:timeupdate=move |ev| {
if let Some(target) = ev.target() {
if let Ok(audio) = target.dyn_into::<web_sys::HtmlAudioElement>() {
let ct = audio.current_time();
let dur = audio.duration();
current_time.set(ct);
if dur.is_finite() && dur > 0.0 {
duration.set(dur);
}
if audio.ended() {
is_playing.set(false);
}
}
}
}
></audio>
<div class="wave-progress">
<div
class="wave-bar"
style=move || format!("width: {}%", progress_pct(current_time.get(), duration.get()))
></div>
</div>
<div class="wave-info">
<button
class=move || format!("btn btn-sm {}", if is_playing.get() { "btn-secondary" } else { "btn-primary" })
on:click=toggle_play
>
{move || if is_playing.get() { "⏸" } else { "▶" }}
</button>
<div class="wave-time">
<span>{move || format_time(current_time.get())}</span>
<span class="wave-title">{title.clone()}</span>
</div>
</div>
</div>
}
}
fn progress_pct(current: f64, dur: f64) -> f64 {
if dur > 0.0 {
(current / dur * 100.0).min(100.0)
} else {
0.0
}
}
fn format_time(secs: f64) -> String {
if !secs.is_finite() || secs < 0.0 {
return "00:00".to_string();
}
let total = secs as u32;
format!("{:02}:{:02}", total / 60, total % 60)
}
@@ -1,4 +0,0 @@
pub mod use_audio_playback;
pub mod use_audio_transmit;
pub mod use_media_control;
pub mod use_voice_control;
@@ -1,117 +0,0 @@
use crate::features::live::audio::pcm_decoder::decode_pcm_frame;
use crate::features::live::audio::ring_buffer::SharedRingBuffer;
use leptos::prelude::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
/// AudioPlaybackState — Manages PCM audio playback from WebSocket binary frames
#[derive(Clone)]
pub struct AudioPlaybackState {
/// Ring buffer for incoming PCM data
pub buffer: SharedRingBuffer,
/// Whether playback is active
pub active: RwSignal<bool>,
/// Volume level (0.0-1.0)
pub volume: RwSignal<f64>,
/// Abort flag to stop the playback loop (prevents leak on teardown)
pub abort: Arc<AtomicBool>,
}
/// Create and initialize audio playback state
pub fn use_audio_playback() -> AudioPlaybackState {
let buffer = SharedRingBuffer::new(44100 * 5); // 5 seconds at 44.1kHz
let active = RwSignal::new(false);
let volume = RwSignal::new(0.5);
AudioPlaybackState {
buffer,
active,
volume,
abort: Arc::new(AtomicBool::new(false)),
}
}
/// Process incoming binary data from WebSocket (PCM audio frame)
/// Format: [u32 userId (4 bytes)][i16 samples (N bytes)]
pub fn process_pcm_data(state: &AudioPlaybackState, data: Vec<u8>) {
if let Some(frame) = decode_pcm_frame(&data) {
state.buffer.write(&frame.samples);
}
}
/// Start consuming the ring buffer and playing through AudioContext
/// The loop respects the abort flag in `AudioPlaybackState` for clean teardown.
pub fn start_playback(state: &AudioPlaybackState) {
if state.active.get_untracked() {
return;
}
state.active.set(true);
// Reset abort flag for a fresh start
state.abort.store(false, Ordering::Relaxed);
let buffer = state.buffer.clone();
let active = state.active;
let abort = state.abort.clone();
wasm_bindgen_futures::spawn_local(async move {
let ctx = match web_sys::AudioContext::new() {
Ok(ctx) => ctx,
Err(_) => {
active.set(false);
return;
}
};
let ctx_ref = &ctx;
let _ = ctx_ref.resume();
while active.get_untracked() && !abort.load(Ordering::Relaxed) {
let available = buffer.available_samples();
if available >= 4410 {
// ~100ms worth at 44.1kHz
let samples = buffer.read(4410);
if !samples.is_empty() {
play_samples(&ctx, &samples);
}
}
let _ = gloo_timers::future::TimeoutFuture::new(50).await;
}
let _ = ctx.close();
});
}
/// Stop playback and clear buffer
pub fn stop_playback(state: &AudioPlaybackState) {
state.abort.store(true, Ordering::Relaxed);
state.active.set(false);
state.buffer.clear();
}
/// Play a chunk of PCM samples through AudioContext using AudioBufferSourceNode
fn play_samples(ctx: &web_sys::AudioContext, samples: &[f32]) {
let frame_count = samples.len() as u32;
let Ok(audio_buffer) = ctx.create_buffer(1, frame_count, ctx.sample_rate()) else {
return;
};
// Write samples into the buffer channel
let Ok(channel_data) = audio_buffer.get_channel_data(0) else {
return;
};
let len = samples.len().min(channel_data.len());
if len == 0 {
return;
}
// Copy samples directly to audio buffer channel
let _ = audio_buffer.copy_to_channel(&samples[..len], 0);
// Create source and play
if let Ok(source) = ctx.create_buffer_source() {
source.set_buffer(Some(&audio_buffer));
source.set_loop(false);
let _ = source.start();
}
}
@@ -1,79 +0,0 @@
use leptos::prelude::*;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::spawn_local;
use web_sys::{MediaStream, MediaStreamConstraints, MediaStreamTrack};
/// AudioTransmitState — Manages microphone capture state
pub struct AudioTransmitState {
pub active: RwSignal<bool>,
pub stream: StoredValue<Option<MediaStream>>,
}
/// Create microphone transmit state
pub fn use_audio_transmit() -> AudioTransmitState {
let active = RwSignal::new(false);
let stream = StoredValue::new(None::<MediaStream>);
AudioTransmitState { active, stream }
}
/// Start microphone capture - requests getUserMedia and stores the stream
pub fn start_transmit(state: &AudioTransmitState) {
if state.active.get_untracked() {
return;
}
state.active.set(true);
let constraints = MediaStreamConstraints::new();
let _ = js_sys::Reflect::set(
&constraints,
&JsValue::from_str("audio"),
&JsValue::from_bool(true),
);
let window = match web_sys::window() {
Some(w) => w,
None => return,
};
let media_devices = match window.navigator().media_devices() {
Ok(md) => md,
Err(_) => return,
};
let promise = match media_devices.get_user_media_with_constraints(&constraints) {
Ok(p) => p,
Err(_) => return,
};
// Clone signals before spawning async task to avoid reference escaping
let active_signal = state.active;
let stream_signal = state.stream;
spawn_local(async move {
match wasm_bindgen_futures::JsFuture::from(promise).await {
Ok(val) => {
if let Ok(s) = val.dyn_into::<MediaStream>() {
stream_signal.set_value(Some(s));
}
}
Err(_) => {
active_signal.set(false);
}
}
});
}
/// Stop microphone transmission
pub fn stop_transmit(state: &AudioTransmitState) {
state.active.set(false);
state.stream.update_value(|s| {
if let Some(stream) = s.take() {
let tracks = stream.get_tracks();
for i in 0..tracks.length() {
let track_val = tracks.get(i);
if let Ok(track) = track_val.dyn_into::<MediaStreamTrack>() {
track.stop();
}
}
}
});
}
Loaded 50 of 165 files, more files were not shown because too many files have changed in this diff. Show more