commit 14a32bdc0d94f71c472f3c06b81065a0c5125619 Author: Dennis Date: Fri Mar 6 00:05:50 2026 +0100 feat: initialize monorepo with full dev team best practices - Unified monorepo with backend (Express), frontend (Next.js), and devops - Backend: ESLint, Prettier, Jest tests (3 passing), health endpoint, .env.example - Frontend: Fixed build errors, fixed all lint errors (0 remaining), tests passing - DevOps: Docker Compose with PostgreSQL, backend, frontend + healthchecks - CI/CD: 3 GitHub Actions workflows (backend, frontend, docker integration) - DX: Husky pre-commit hooks with smart change detection - Docs: Root README with architecture, CONTRIBUTING.md, PR template Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..68b31a4 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# ============================================ +# Website Monitoring — Environment Variables +# ============================================ +# Copy this file to .env and fill in your values. +# Used by docker-compose and local development. + +# ── PostgreSQL ────────────────────────────── +POSTGRES_USER=monitoring +POSTGRES_PASSWORD=monitoring_pass +POSTGRES_DB=monitoring + +# ── Backend ───────────────────────────────── +PORT=5000 +DATABASE_URL=postgresql://monitoring:monitoring_pass@localhost:5432/monitoring +CORS_ORIGIN=http://localhost:3000 + +# ── Frontend (Supabase) ──────────────────── +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key +SUPABASE_SERVICE_ROLE_KEY=your-service-role-key diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..2600b3a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,40 @@ +## Description + + + +Closes # + +## Type of Change + +- [ ] 🐛 Bug fix (non-breaking change fixing an issue) +- [ ] ✨ New feature (non-breaking change adding functionality) +- [ ] 💥 Breaking change (fix or feature causing existing functionality to change) +- [ ] 📝 Documentation update +- [ ] 🧹 Chore (dependency update, refactor, etc.) + +## Changes Made + + + +- +- +- + +## Testing + +- [ ] Existing tests pass (`npm test`) +- [ ] New tests added for new functionality +- [ ] Tested locally with `npm run dev` +- [ ] Tested with Docker (`npm run docker:up`) + +## Checklist + +- [ ] My code follows the project code style +- [ ] I've run `npm run lint` with no errors +- [ ] I've updated documentation if needed +- [ ] I've updated `.env.example` if new env vars were added +- [ ] No secrets or credentials in the code + +## Screenshots (if applicable) + + diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 0000000..8b37210 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,56 @@ +name: Backend CI + +on: + push: + branches: [main, develop] + paths: + - "website-monitoring-backend/**" + - ".github/workflows/backend.yml" + pull_request: + branches: [main] + paths: + - "website-monitoring-backend/**" + +defaults: + run: + working-directory: website-monitoring-backend + +jobs: + lint-test-build: + name: Lint, Test & Build + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: website-monitoring-backend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Test + run: npm test + env: + NODE_ENV: test + + - name: Build + run: npm run build + + - name: Upload coverage + if: matrix.node-version == 20 + uses: actions/upload-artifact@v4 + with: + name: backend-coverage + path: website-monitoring-backend/coverage/ + retention-days: 7 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..cc8c1b8 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,31 @@ +name: Docker Integration + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + docker-build: + name: Docker Compose Build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build all services + run: docker compose -f website-monitoring-devops/docker-compose.yml build + + - name: Start services + run: | + docker compose -f website-monitoring-devops/docker-compose.yml up -d db backend + sleep 15 + + - name: Verify backend health + run: | + docker compose -f website-monitoring-devops/docker-compose.yml exec -T backend curl -f http://localhost:5000/health || exit 1 + + - name: Cleanup + if: always() + run: docker compose -f website-monitoring-devops/docker-compose.yml down -v diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 0000000..3e8f86a --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,51 @@ +name: Frontend CI + +on: + push: + branches: [main, develop] + paths: + - "website-monitoring-frontend/**" + - ".github/workflows/frontend.yml" + pull_request: + branches: [main] + paths: + - "website-monitoring-frontend/**" + +defaults: + run: + working-directory: website-monitoring-frontend + +jobs: + lint-test-build: + name: Lint, Test & Build + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: website-monitoring-frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Test + run: npm test + env: + NODE_ENV: test + + - name: Build + run: npm run build + env: + NEXT_PUBLIC_SUPABASE_URL: https://placeholder.supabase.co + NEXT_PUBLIC_SUPABASE_ANON_KEY: placeholder-key diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..513b64e --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Dependencies +node_modules/ +.pnp.* + +# Build outputs +dist/ +.next/ +out/ +build/ +*.tsbuildinfo + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* + +# Testing +coverage/ +.nyc_output/ + +# Docker +*.pid +*.seed + +# Misc +.cache/ +.parcel-cache/ +.eslintcache diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..3c31ba8 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,17 @@ +#!/bin/sh + +# Check if backend files changed +if git diff --cached --name-only | grep -q "^website-monitoring-backend/"; then + echo "🔍 Linting backend..." + cd website-monitoring-backend && npx eslint src/ || exit 1 + cd .. +fi + +# Check if frontend files changed +if git diff --cached --name-only | grep -q "^website-monitoring-frontend/"; then + echo "🔍 Linting frontend..." + cd website-monitoring-frontend && npx next lint || exit 1 + cd .. +fi + +echo "✅ Pre-commit checks passed" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a0c8495 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,87 @@ +# Contributing to Website Monitoring + +## Branch Strategy + +``` +main ← production-ready, protected + └─ develop ← integration branch + └─ feature/xxx ← your work + └─ fix/xxx ← bug fixes + └─ chore/xxx ← maintenance +``` + +### Rules +- **Never push directly to `main`** — always open a PR +- Branch from `develop` for features, from `main` for hotfixes +- Delete branches after merge + +## Development Setup + +```bash +# Clone & install +git clone +cd website-monitoring +npm install + +# Install project dependencies +cd website-monitoring-backend && npm install && cd .. +cd website-monitoring-frontend && npm install && cd .. + +# Start everything +npm run dev +``` + +## Workflow + +1. **Create a branch**: `git checkout -b feature/my-feature develop` +2. **Make changes**: Write code, add tests +3. **Verify locally**: + ```bash + npm run lint # No errors + npm run test # All pass + npm run build # Compiles clean + ``` +4. **Commit**: Use [Conventional Commits](https://www.conventionalcommits.org/) + ``` + feat: add uptime monitoring endpoint + fix: resolve SSE connection leak + chore: update lighthouse dependency + docs: add API endpoint documentation + test: add lighthouse service unit tests + ``` +5. **Push & open PR**: Target `develop` (or `main` for hotfixes) + +## Code Review Checklist + +- [ ] Code compiles and all tests pass +- [ ] New code has tests +- [ ] No `console.log` left in production code (use proper logger) +- [ ] TypeScript types are explicit (minimize `any`) +- [ ] API changes are documented +- [ ] Environment variables documented in `.env.example` +- [ ] No secrets committed + +## Code Style + +- **TypeScript** everywhere (no plain JS) +- **ESLint** for linting — `npm run lint` +- **Prettier** for formatting — auto-runs on commit via Husky +- Use `async/await` over `.then()` chains +- Prefer named exports over default exports + +## Testing + +- **Backend**: Jest + Supertest — `cd website-monitoring-backend && npm test` +- **Frontend**: Jest + Testing Library — `cd website-monitoring-frontend && npm test` +- Aim for tests on all API endpoints and critical business logic +- Use meaningful test names: `it("should return 400 when URL is missing")` + +## Docker + +```bash +# Full stack with Docker Compose +npm run docker:up + +# Tear down +npm run docker:down +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ecbcfa --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# 🔍 Website Monitoring Platform + +Full-stack website monitoring platform that uses **Google Lighthouse** to audit performance, SEO, accessibility, and best practices — with real-time progress tracking, team collaboration, and alerting. + +``` +┌─────────────────────────────────────────────────────────┐ +│ Website Monitoring │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ +│ │ Frontend │───▶│ Backend │───▶│ PostgreSQL (DB) │ │ +│ │ Next.js │ │ Express │ │ via Supabase │ │ +│ │ Port 3000 │◀───│ Port 5000│ └──────────────────┘ │ +│ └──────────┘ └──────────┘ │ +│ │ │ │ +│ │ ▼ │ +│ │ ┌──────────┐ │ +│ │ │Lighthouse│ │ +│ └─── SSE ──│ + Chrome │ │ +│ (progress) │ Headless │ │ +│ └──────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +## ✨ Features + +| Feature | Status | Description | +|---------|--------|-------------| +| **Lighthouse Audits** | ✅ Real | Performance, SEO, accessibility, best practices scores | +| **Real-time Progress** | ✅ Real | Server-Sent Events stream during scans | +| **Dashboard** | ✅ Real | Overview with charts, metrics, and website list | +| **Performance Monitoring** | ✅ Real | Track scores over time with Recharts | +| **SEO Analysis** | ✅ Real | SEO score tracking and recommendations | +| **Uptime Monitoring** | ✅ Real | Periodic uptime checks with alerting | +| **Team/Organization** | ✅ Real | Multi-user orgs with role-based access | +| **Alerts** | ✅ Real | Configurable alerts for downtime, performance, SSL | +| **Competitor Analysis** | ✅ Real | Compare your site metrics against competitors | +| **Authentication** | ✅ Real | Supabase Auth (email, OAuth) | +| **Scheduled Scans** | ✅ Real | Cron-based automated Lighthouse scans every 6 hours | +| **Website Crawler** | ✅ Real | Crawl and discover pages on your websites | + +## 🛠 Tech Stack + +| Layer | Technology | Cost | +|-------|-----------|------| +| Frontend | Next.js 15, React 19, TypeScript, Tailwind CSS 4, Shadcn/UI | Free | +| Backend | Express.js, TypeScript, Node.js 18+ | Free | +| Database | PostgreSQL via Supabase | Free tier | +| Auth | Supabase Auth | Free tier | +| Auditing | Google Lighthouse + Headless Chrome | Free (OSS) | +| Charts | Recharts + Chart.js | Free (OSS) | +| CI/CD | GitHub Actions | Free (public repos) | +| Containers | Docker + Docker Compose | Free | +| Linting | ESLint + Prettier | Free (OSS) | +| Testing | Jest + Supertest + Testing Library | Free (OSS) | +| Pre-commit | Husky + lint-staged | Free (OSS) | + +## 🚀 Quick Start + +### Prerequisites +- Node.js 18+ +- npm +- Docker & Docker Compose (for full stack) +- Supabase account (free tier) + +### Option 1: Local Development + +```bash +# Clone the repo +git clone +cd website-monitoring + +# Install root dependencies (Husky, concurrently) +npm install + +# Setup backend +cd website-monitoring-backend +cp .env.example .env +npm install +npm run build +cd .. + +# Setup frontend +cd website-monitoring-frontend +cp .env.example .env # Fill in your Supabase keys +npm install +cd .. + +# Run everything +npm run dev +``` + +### Option 2: Docker Compose + +```bash +# Copy and fill in environment variables +cp .env.example .env + +# Start all services +npm run docker:up + +# Access at http://localhost:3000 +``` + +## 📁 Project Structure + +``` +website-monitoring/ +├── website-monitoring-backend/ # Express.js API + Lighthouse engine +│ ├── src/ +│ │ ├── index.ts # Server entry, health check, routing +│ │ └── routes/ +│ │ └── lighthouse.ts # Lighthouse audit + SSE progress +│ ├── Dockerfile +│ └── package.json +│ +├── website-monitoring-frontend/ # Next.js 15 dashboard +│ ├── src/ +│ │ ├── app/ # Pages & API routes (20+ endpoints) +│ │ ├── components/ # React components (dashboard, UI, auth) +│ │ ├── services/ # Business logic (scanning, monitoring) +│ │ └── types/ # TypeScript type definitions +│ ├── Dockerfile +│ └── package.json +│ +├── website-monitoring-devops/ # Infrastructure +│ ├── docker-compose.yml # Full stack orchestration +│ └── .devcontainer/ # VS Code Dev Container config +│ +├── .github/ +│ ├── workflows/ +│ │ ├── backend.yml # Backend CI: lint, test, build +│ │ ├── frontend.yml # Frontend CI: lint, test, build +│ │ └── docker.yml # Docker Compose integration test +│ └── pull_request_template.md +│ +├── CONTRIBUTING.md # Branch strategy, code review, guidelines +├── .env.example # Unified environment template +└── package.json # Root scripts (dev, build, test, lint) +``` + +## 🧪 Testing + +```bash +# Run all tests +npm test + +# Backend only (3 tests: health, API info, validation) +npm run test:backend + +# Frontend only (component tests) +npm run test:frontend +``` + +## 📊 CI/CD Pipelines + +| Workflow | Trigger | What it does | +|----------|---------|-------------| +| `backend.yml` | Push/PR to backend | Lint → Test → Build (Node 18 & 20) | +| `frontend.yml` | Push/PR to frontend | Lint → Test → Build (Node 18 & 20) | +| `docker.yml` | Push/PR to main | Docker Compose build → Backend health check | + +## 🤝 Contributing + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for branch strategy, code style, and PR process. + +## 📄 License + +ISC diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5c596ad --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1185 @@ +{ + "name": "website-monitoring", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "website-monitoring", + "version": "1.0.0", + "devDependencies": { + "concurrently": "^9.1.0", + "husky": "^9.1.0", + "lint-staged": "^15.4.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..70e99fd --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "website-monitoring", + "version": "1.0.0", + "private": true, + "description": "Full-stack website monitoring platform with Lighthouse performance auditing", + "scripts": { + "dev:backend": "cd website-monitoring-backend && npm run dev", + "dev:frontend": "cd website-monitoring-frontend && npm run dev", + "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"", + "build:backend": "cd website-monitoring-backend && npm run build", + "build:frontend": "cd website-monitoring-frontend && npm run build", + "build": "npm run build:backend && npm run build:frontend", + "test:backend": "cd website-monitoring-backend && npm test", + "test:frontend": "cd website-monitoring-frontend && npm test", + "test": "npm run test:backend && npm run test:frontend", + "lint:backend": "cd website-monitoring-backend && npm run lint", + "lint:frontend": "cd website-monitoring-frontend && npm run lint", + "lint": "npm run lint:backend && npm run lint:frontend", + "docker:up": "docker compose -f website-monitoring-devops/docker-compose.yml up --build", + "docker:down": "docker compose -f website-monitoring-devops/docker-compose.yml down", + "prepare": "husky" + }, + "devDependencies": { + "concurrently": "^9.1.0", + "husky": "^9.1.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/website-monitoring-backend/.env.example b/website-monitoring-backend/.env.example new file mode 100644 index 0000000..51c0e5b --- /dev/null +++ b/website-monitoring-backend/.env.example @@ -0,0 +1,11 @@ +# Server +PORT=5000 + +# Database (PostgreSQL) +DATABASE_URL=postgresql://user:password@localhost:5432/monitoring + +# CORS +CORS_ORIGIN=http://localhost:3000 + +# Chrome (for Docker/CI) +CHROME_PATH=/usr/bin/chromium diff --git a/website-monitoring-backend/.gitignore b/website-monitoring-backend/.gitignore new file mode 100644 index 0000000..c6bba59 --- /dev/null +++ b/website-monitoring-backend/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/website-monitoring-backend/.prettierrc b/website-monitoring-backend/.prettierrc new file mode 100644 index 0000000..cf7c9eb --- /dev/null +++ b/website-monitoring-backend/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": false, + "printWidth": 100, + "tabWidth": 2 +} diff --git a/website-monitoring-backend/Dockerfile b/website-monitoring-backend/Dockerfile new file mode 100644 index 0000000..0c02b3a --- /dev/null +++ b/website-monitoring-backend/Dockerfile @@ -0,0 +1,25 @@ +# Use the official Node.js image. +FROM node:18 + +# OPTIONAL: Falls in der Base kein Chrome enthalten ist, +# müsstest du hier noch "apt-get update" + "apt-get install chromium" oder ähnliches ausführen, +# z. B.: +RUN apt-get update && apt-get install -y chromium + +# Create and change to the app directory. +WORKDIR /app + +# Copy application dependency manifests to the container image. +COPY package*.json ./ + +# Install production dependencies. +RUN npm install + +# Copy local code to the container image. +COPY . . + +# Build the TypeScript code +RUN npm run build + +# Run the web service on container startup. +CMD ["node", "dist/index.js"] diff --git a/website-monitoring-backend/README.md b/website-monitoring-backend/README.md new file mode 100644 index 0000000..ab617f1 --- /dev/null +++ b/website-monitoring-backend/README.md @@ -0,0 +1,57 @@ +# Website Monitoring Backend + +Express.js API server that runs Google Lighthouse audits on websites and streams real-time progress via Server-Sent Events. + +## Tech Stack + +- **Runtime**: Node.js 18+ +- **Framework**: Express.js +- **Language**: TypeScript +- **Auditing**: Google Lighthouse + Chrome Headless +- **Database**: PostgreSQL (via `pg`) + +## Quick Start + +```bash +cp .env.example .env +npm install +npm run build +npm start +``` + +## Scripts + +| Script | Description | +|--------|-------------| +| `npm run build` | Compile TypeScript to `dist/` | +| `npm start` | Run the production server | +| `npm run dev` | Watch mode for development | +| `npm test` | Run Jest tests | +| `npm run test:coverage` | Run tests with coverage report | +| `npm run lint` | Run ESLint | +| `npm run format` | Format code with Prettier | + +## API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/` | API info | +| `GET` | `/health` | Health check | +| `POST` | `/api/lighthouse` | Start Lighthouse audit (body: `{ "url": "https://example.com" }`) | +| `GET` | `/api/lighthouse/status/:id` | SSE stream for audit progress | + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `PORT` | `5000` | Server port | +| `DATABASE_URL` | — | PostgreSQL connection string | +| `CORS_ORIGIN` | `*` | Allowed CORS origin | +| `CHROME_PATH` | — | Path to Chrome binary (Docker) | + +## Docker + +```bash +docker build -t website-monitoring-backend . +docker run -p 5000:5000 website-monitoring-backend +``` \ No newline at end of file diff --git a/website-monitoring-backend/eslint.config.mjs b/website-monitoring-backend/eslint.config.mjs new file mode 100644 index 0000000..cdde35d --- /dev/null +++ b/website-monitoring-backend/eslint.config.mjs @@ -0,0 +1,23 @@ +import eslint from "@typescript-eslint/eslint-plugin"; +import parser from "@typescript-eslint/parser"; + +export default [ + { + files: ["src/**/*.ts"], + languageOptions: { + parser, + parserOptions: { + ecmaVersion: 2022, + sourceType: "module", + }, + }, + plugins: { + "@typescript-eslint": eslint, + }, + rules: { + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-explicit-any": "warn", + "no-console": "off", + }, + }, +]; diff --git a/website-monitoring-backend/jest.config.mjs b/website-monitoring-backend/jest.config.mjs new file mode 100644 index 0000000..2f69824 --- /dev/null +++ b/website-monitoring-backend/jest.config.mjs @@ -0,0 +1,21 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +export default { + preset: "ts-jest/presets/default-esm", + testEnvironment: "node", + roots: ["/src"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + useESM: true, + }, + ], + }, + extensionsToTreatAsEsm: [".ts"], + collectCoverageFrom: ["src/**/*.ts", "!src/**/*.test.ts"], + coverageDirectory: "coverage", + coverageReporters: ["text", "lcov", "clover"], +}; diff --git a/website-monitoring-backend/package-lock.json b/website-monitoring-backend/package-lock.json new file mode 100644 index 0000000..f8840e6 --- /dev/null +++ b/website-monitoring-backend/package-lock.json @@ -0,0 +1,8840 @@ +{ + "name": "website-monitoring-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "website-monitoring-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chrome-launcher": "^1.1.2", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "lighthouse": "^12.4.0", + "lighthouse-logger": "^2.0.1", + "pg": "^8.13.3", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^30.0.0", + "@types/node": "^22.13.9", + "@types/supertest": "^7.2.0", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^10.0.2", + "eslint-config-prettier": "^10.1.8", + "jest": "^30.2.0", + "prettier": "^3.8.1", + "supertest": "^7.2.2", + "ts-jest": "^29.4.6" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", + "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.2", + "debug": "^4.3.1", + "minimatch": "^10.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", + "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.3.tgz", + "integrity": "sha512-pJT1OkhplSmvvr6i3CWTPvC/FGC06MbN5TNBfRO6Ox62AEz90eMq+dVvtX9Bl3jxCEkS0tATzDarRZuOLw7oFg==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/intl-localematcher": "0.6.0", + "decimal.js": "10", + "tslib": "2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz", + "integrity": "sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.1.tgz", + "integrity": "sha512-o0AhSNaOfKoic0Sn1GkFCK4MxdRsw7mPJ5/rBpIqdvcC7MIuyUSW8WChUEvrK78HhNpYOgqCQbINxCTumJLzZA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.3", + "@formatjs/icu-skeleton-parser": "1.8.13", + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.13", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.13.tgz", + "integrity": "sha512-N/LIdTvVc1TpJmMt2jVg0Fr1F7Q1qJPdZSCs19unMskCmVQ/sa0H9L8PWt13vq+gLdLg1+pPsvBLydL1Apahjg==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.3", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.0.tgz", + "integrity": "sha512-4rB4g+3hESy1bHSBG3tDFaMY2CH67iT7yne1e+0CLTsGLDcmoEWWpJjjpWVaYgYfYuohIRuo0E+N536gd2ZHZA==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/transform/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@paulirish/trace_engine": { + "version": "0.0.44", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.44.tgz", + "integrity": "sha512-QjDv5qVaUXd5WZzE2ktKvqtGA17v4HFtj6MROCGkK57AZr9n0ZKgcx7dEFho+5EHZ6V6h1upW2eqheo8C4Y4dA==", + "license": "BSD-3-Clause", + "dependencies": { + "third-party-web": "latest" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.7.1.tgz", + "integrity": "sha512-MK7rtm8JjaxPN7Mf1JdZIZKPD2Z+W7osvrC1vjpvfOX1K0awDIHYbNi89f7eotp7eMUn2shWnt03HwVbriXtKQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.0", + "tar-fs": "^3.0.8", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.3.tgz", + "integrity": "sha512-Ausx+Jw1pAMbIBHStoQ6ZqDZR60PsCByvHdw/jdH9AqPrNE9xlBSf9EwcycvmrzwyKspSLaB52grlje2cRIUMg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.3.tgz", + "integrity": "sha512-vyy11fCGpkGK3qI5DSXOjgIboBZTriw0YDx/0KyX5CjIjDDNgp5AGgpgFkfZyiYiaU2Ww3iFuKo4wHmBusz1uA==", + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/integrations": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.3.tgz", + "integrity": "sha512-6i/lYp0BubHPDTg91/uxHvNui427df9r17SsIEXa2eKDwQ9gW2qRx5IWgvnxs2GV/GfSbwcx4swUB3RfEWrXrQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3", + "localforage": "^1.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.3.tgz", + "integrity": "sha512-t+QtekZedEfiZjbkRAk1QWJPnJlFBH/ti96tQhEq7wmlk3VszDXraZvLWZA0P2vXyglKzbWRGkT31aD3/kX+5Q==", + "license": "MIT", + "dependencies": { + "@sentry-internal/tracing": "7.120.3", + "@sentry/core": "7.120.3", + "@sentry/integrations": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/types": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.3.tgz", + "integrity": "sha512-C4z+3kGWNFJ303FC+FxAd4KkHvxpNFYAFN8iMIgBwJdpIl25KZ8Q/VdGn0MLLUEHNLvjob0+wvwlcRBBNLXOow==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.3.tgz", + "integrity": "sha512-UDAOQJtJDxZHQ5Nm1olycBIsz2wdGX8SdzyGVHmD8EOQYAeDZQyIlQYohDe9nazdIOQLZCIc3fU0G9gqVLkaGQ==", + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz", + "integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/argparse/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axe-core": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.1.tgz", + "integrity": "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^3.0.0", + "bare-stream": "^2.0.0" + }, + "engines": { + "bare": ">=1.7.0" + } + }, + "node_modules/bare-os": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.5.1.tgz", + "integrity": "sha512-LvfVNDcWLw2AnIw5f2mWUgumW3I3N/WYGiWeimhQC1Ybt71n2FjlS9GJKeCnFeg1MKZHxzIFmpFnBXDI+sBeFg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001776", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", + "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-launcher": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.2.tgz", + "integrity": "sha512-YclTJey34KUm5jB1aEJCq807bSievi7Nb/TU4Gu504fUYi3jw3KCIaH6L7nFWQhdEgH3V+wCh+kKD1P5cXnfxw==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-bidi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-2.0.0.tgz", + "integrity": "sha512-8VmyVj0ewSY4pstZV0Y3rCUUwpomam8uWgHZf1XavRxJEP4vU9/dcpNuoyB+u4AQxPo96CASXz5CHPvdH+dSeQ==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/csp_evaluator": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.5.tgz", + "integrity": "sha512-EL/iN9etCTzw/fBnp0/uj0f5BOOGvZut2mzsiiBZ/FdT6gFQCKRO/tmcKOxn5drWZ2Ndm/xBb1SI4zwWbGtmIw==", + "license": "Apache-2.0" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1423531", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1423531.tgz", + "integrity": "sha512-z6cOcajZWxk80zvFnkTGa7tj3oqF+C5SnOF1KSMeAr5/WW/nLNHlEpKr7voSzMz8IaUoq5rjdI0Mqv5k/BUkhg==", + "license": "BSD-3-Clause" + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", + "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.2", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.1", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.1", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", + "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", + "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-link-header": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.3.tgz", + "integrity": "sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "license": "MIT" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/intl-messageformat": { + "version": "10.7.15", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.15.tgz", + "integrity": "sha512-LRyExsEsefQSBjU2p47oAheoKz+EOJxSLDdjOaEjdriajfHsMXOmV/EhMvYSg9bAgCUHasuAC+mcUBe/95PfIg==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.3", + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/icu-messageformat-parser": "2.11.1", + "tslib": "2" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/js-library-detector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", + "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lighthouse": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.4.0.tgz", + "integrity": "sha512-1p/YKQpMqfYVSKVOB43RG3xbnxkSUOG0zqVm/bxJHAaAHKrEACgFi8HZxD9CCTFrt+d/Q/x9gjDyeUDarm1SIg==", + "license": "Apache-2.0", + "dependencies": { + "@paulirish/trace_engine": "0.0.44", + "@sentry/node": "^7.0.0", + "axe-core": "^4.10.2", + "chrome-launcher": "^1.1.2", + "configstore": "^5.0.1", + "csp_evaluator": "1.1.5", + "devtools-protocol": "0.0.1423531", + "enquirer": "^2.3.6", + "http-link-header": "^1.1.1", + "intl-messageformat": "^10.5.3", + "jpeg-js": "^0.4.4", + "js-library-detector": "^6.7.0", + "lighthouse-logger": "^2.0.1", + "lighthouse-stack-packs": "1.12.2", + "lodash-es": "^4.17.21", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.3.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "puppeteer-core": "^24.3.0", + "robots-parser": "^3.0.1", + "semver": "^5.3.0", + "speedline-core": "^1.4.3", + "third-party-web": "^0.26.5", + "tldts-icann": "^6.1.16", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "chrome-debug": "core/scripts/manual-chrome-launcher.js", + "lighthouse": "cli/index.js", + "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" + }, + "engines": { + "node": ">=18.16" + } + }, + "node_modules/lighthouse-logger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", + "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-stack-packs": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.2.tgz", + "integrity": "sha512-Ug8feS/A+92TMTCK6yHYLwaFMuelK/hAKRMdldYkMNwv+d9PtWxjXEg6rwKtsUXTADajhdrhXyuNCJ5/sfmPFw==", + "license": "Apache-2.0" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/metaviewport-parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.13.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz", + "integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.1", + "pg-protocol": "^1.7.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz", + "integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz", + "integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer-core": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.3.0.tgz", + "integrity": "sha512-x8kQRP/xxtiFav6wWuLzrctO0HWRpSQy+JjaHbqIl+d5U2lmRh2pY9vh5AzDFN0EtOXW2pzngi9RrryY1vZGig==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.7.1", + "chromium-bidi": "2.0.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1402036", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1402036", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1402036.tgz", + "integrity": "sha512-JwAYQgEvm3yD45CHB+RmF5kMbWtXBaOGwuxa87sZogHcLCv8c/IqnThaoQ1y60d7pXWjSKWQphPEc+1rAScVdg==", + "license": "BSD-3-Clause" + }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamx": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", + "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/superagent/node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tar-fs": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", + "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/third-party-web": { + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.26.5.tgz", + "integrity": "sha512-tDuKQJUTfjvi9Fcrs1s6YAQAB9mzhTSbBZMfNgtWNmJlHuoFeXO6dzBFdGeCWRvYL50jQGK0jPsBZYxqZQJ2SA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tldts-core": { + "version": "6.1.82", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.82.tgz", + "integrity": "sha512-Jabl32m21tt/d/PbDO88R43F8aY98Piiz6BVH9ShUlOAiiAELhEqwrAmBocjAqnCfoUeIsRU+h3IEzZd318F3w==", + "license": "MIT" + }, + "node_modules/tldts-icann": { + "version": "6.1.82", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-6.1.82.tgz", + "integrity": "sha512-PTNEhlIL2K6uNakhKtuP5+PgmfZ03AJ3QOOrWvZH3aAiWMioaANN0WsFy8t/JN+z/znEwziBYJ06PxHNV2rN7Q==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.82" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/website-monitoring-backend/package.json b/website-monitoring-backend/package.json new file mode 100644 index 0000000..8908e33 --- /dev/null +++ b/website-monitoring-backend/package.json @@ -0,0 +1,55 @@ +{ + "name": "website-monitoring-backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsc --watch", + "test": "NODE_OPTIONS='--experimental-vm-modules' jest --passWithNoTests", + "test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "format": "prettier --write src/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/denshooter/website-monitoring-backend.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/denshooter/website-monitoring-backend/issues" + }, + "homepage": "https://github.com/denshooter/website-monitoring-backend#readme", + "dependencies": { + "chrome-launcher": "^1.1.2", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "lighthouse": "^12.4.0", + "lighthouse-logger": "^2.0.1", + "pg": "^8.13.3", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^30.0.0", + "@types/node": "^22.13.9", + "@types/supertest": "^7.2.0", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^10.0.2", + "eslint-config-prettier": "^10.1.8", + "jest": "^30.2.0", + "prettier": "^3.8.1", + "supertest": "^7.2.2", + "ts-jest": "^29.4.6" + } +} diff --git a/website-monitoring-backend/src/__tests__/api.test.ts b/website-monitoring-backend/src/__tests__/api.test.ts new file mode 100644 index 0000000..fc4938e --- /dev/null +++ b/website-monitoring-backend/src/__tests__/api.test.ts @@ -0,0 +1,32 @@ +import request from "supertest"; +import { app } from "../index.js"; + +describe("API Server", () => { + describe("GET /health", () => { + it("should return 200 with status ok", async () => { + const res = await request(app).get("/health"); + expect(res.status).toBe(200); + expect(res.body.status).toBe("ok"); + expect(res.body.timestamp).toBeDefined(); + }); + }); + + describe("GET /", () => { + it("should return API info", async () => { + const res = await request(app).get("/"); + expect(res.status).toBe(200); + expect(res.body.name).toBe("Website Monitoring API"); + expect(res.body.version).toBe("1.0.0"); + expect(res.body.endpoints).toContain("/health"); + expect(res.body.endpoints).toContain("/api/lighthouse"); + }); + }); + + describe("POST /api/lighthouse", () => { + it("should return 400 when no URL provided", async () => { + const res = await request(app).post("/api/lighthouse").send({}); + expect(res.status).toBe(400); + expect(res.body.error).toBe("Missing URL"); + }); + }); +}); diff --git a/website-monitoring-backend/src/index.ts b/website-monitoring-backend/src/index.ts new file mode 100644 index 0000000..e27ffef --- /dev/null +++ b/website-monitoring-backend/src/index.ts @@ -0,0 +1,32 @@ +import express, { Request, Response } from "express"; +import cors from "cors"; +import dotenv from "dotenv"; +import lighthouseRouter from "./routes/lighthouse.js"; + +dotenv.config(); + +const app = express(); + +app.use(cors({ origin: process.env.CORS_ORIGIN || "*" })); +app.use(express.json()); + +app.get("/health", (_req: Request, res: Response) => { + res.status(200).json({ status: "ok", timestamp: new Date().toISOString() }); +}); + +app.get("/", (_req: Request, res: Response) => { + res.status(200).json({ + name: "Website Monitoring API", + version: "1.0.0", + endpoints: ["/health", "/api/lighthouse"], + }); +}); + +app.use("/api/lighthouse", lighthouseRouter); + +export { app }; + +if (process.env.NODE_ENV !== "test") { + const PORT = process.env.PORT || 5000; + app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); +} diff --git a/website-monitoring-backend/src/routes/lighthouse.ts b/website-monitoring-backend/src/routes/lighthouse.ts new file mode 100644 index 0000000..d18d8b6 --- /dev/null +++ b/website-monitoring-backend/src/routes/lighthouse.ts @@ -0,0 +1,174 @@ +import express, { Request, Response } from "express"; +import { v4 as uuidv4 } from "uuid"; + +const router = express.Router(); +const progressClients = new Map(); + +/** Send SSE progress data to the browser */ +function sendProgress(clientId: string, data: any) { + const client = progressClients.get(clientId); + if (client) { + client.write(`data: ${JSON.stringify(data)}\n\n`); + } +} + +// SSE endpoint for progress +router.get("/status/:id", (req: Request, res: Response) => { + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.setHeader("Access-Control-Allow-Origin", "*"); + + const clientId = req.params.id; + progressClients.set(clientId, res); + + // Send an initial event + sendProgress(clientId, { + status: "Connected", + progress: 0, + stage: "setup", + }); + + req.on("close", () => { + progressClients.delete(clientId); + }); +}); + +router.post("/", async (req: Request, res: Response) => { + const { url } = req.body; + if (!url) { + return res.status(400).json({ error: "Missing URL" }); + } + + const clientId = uuidv4(); // Generate a UUID + console.log(`New client ID: ${clientId}`); + let currentProgress = 0; + let auditCount = 0; + let totalAudits = 0; + + try { + // Immediately tell the frontend we’re starting (0%) + sendProgress(clientId, { + status: "Starting analysis...", + progress: 0, + stage: "setup", + }); + + // Return clientId so the frontend can open the SSE channel + res.status(200).json({ clientId }); + + // Dynamically import chrome-launcher and lighthouse + const { launch } = await import("chrome-launcher"); + const lighthouse = (await import("lighthouse")).default; + const log = (await import("lighthouse-logger")).default; + + // Launch Chrome + const chrome = await launch({ + chromeFlags: ["--headless", "--no-sandbox", "--disable-dev-shm-usage"], + }); + + // Update progress to 10% after launching Chrome + currentProgress = 10; + sendProgress(clientId, { + status: "Chrome launched", + progress: currentProgress, + stage: "setup", + }); + + // Turn on Lighthouse logs at "info" level + log.setLevel("info"); + + // Listen for Lighthouse status events + log.events.addListener("status", (lhStatus) => { + let msg = lhStatus[1].toLowerCase(); + let newProgress = currentProgress; + let newStage = "analyzing"; + + if (msg.includes("initialize config")) { + newProgress = Math.max(newProgress, 15); + newStage = "setup"; + } else if (msg.includes("resolve artifact definitions")) { + newProgress = Math.max(newProgress, 20); + newStage = "setup"; + } else if (msg.includes("gather phase")) { + newProgress = Math.max(newProgress, 25); + newStage = "analyzing"; + } else if (msg.includes("connecting to browser")) { + newProgress = Math.max(newProgress, 28); + newStage = "analyzing"; + } else if (msg.includes("navigating to")) { + newProgress = Math.max(newProgress, 30); + newStage = "analyzing"; + } else if (msg.includes("benchmarking machine")) { + newProgress = Math.max(newProgress, 35); + newStage = "analyzing"; + } else if (msg.includes("getting artifact")) { + newProgress = Math.max(newProgress, 40); + newStage = "analyzing"; + } else if (msg.includes("computing artifact")) { + newProgress = Math.max(newProgress, 50); + newStage = "analyzing"; + } else if (msg.includes("audit phase")) { + newProgress = Math.max(newProgress, 60); + newStage = "auditing"; + } else if (msg.includes("auditing:")) { + auditCount++; + totalAudits = Math.max(totalAudits, auditCount); + newProgress = Math.max( + newProgress, + 60 + (auditCount / totalAudits) * 30, + ); + newStage = "auditing"; + } else if (msg.includes("generating results")) { + newProgress = Math.max(newProgress, 95); + newStage = "finishing"; + } + + // Add some randomness to the progress increments + newProgress += Math.random() * 2; + + // Only send progress if it actually increased + if (newProgress > currentProgress) { + currentProgress = newProgress; + sendProgress(clientId, { + status: lhStatus.message || "Processing...", + progress: currentProgress, + stage: newStage, + }); + } + }); + + // Run Lighthouse + const runnerResult = await lighthouse(url, { + port: chrome.port, + output: "json", + logLevel: "info", + onlyCategories: ["performance", "seo", "accessibility", "best-practices"], + }); + + if (!runnerResult) { + throw new Error("Lighthouse returned no result"); + } + + // Finish progress + sendProgress(clientId, { + status: "Analysis complete", + progress: 100, + stage: "complete", + report: runnerResult.lhr, + }); + + await chrome.kill(); + } catch (error) { + console.error("Lighthouse error:", error); + sendProgress(clientId, { + status: "Error occurred", + progress: 0, + stage: "error", + error: + error instanceof Error ? error.message : "An unknown error occurred", + }); + } +}); + +export default router; diff --git a/website-monitoring-backend/tsconfig.json b/website-monitoring-backend/tsconfig.json new file mode 100644 index 0000000..ca1f6b8 --- /dev/null +++ b/website-monitoring-backend/tsconfig.json @@ -0,0 +1,14 @@ +// filepath: /c:/Users/denni/OneDrive/Dokumente/code/website-monitoring/backend/tsconfig.json +{ + "compilerOptions": { + "module": "ESNext", + "target": "es2018", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/website-monitoring-devops/.devcontainer/devcontainer.json b/website-monitoring-devops/.devcontainer/devcontainer.json new file mode 100644 index 0000000..3c015b9 --- /dev/null +++ b/website-monitoring-devops/.devcontainer/devcontainer.json @@ -0,0 +1,35 @@ +{ + "name": "Website Monitoring Development", + "image": "mcr.microsoft.com/devcontainers/javascript-node:0-18", + "workspaceFolder": "/workspaces/devops", + + // Dies funktioniert sowohl lokal als auch in Codespaces + "workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind,consistency=cached", + + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss", + "ms-vscode.vscode-typescript-next" + ], + "settings": { + "terminal.integrated.defaultProfile.linux": "bash" + } + } + }, + "forwardPorts": [3000, 5000], + "remoteEnv": { + "NEXT_PUBLIC_SUPABASE_URL": "${localEnv:NEXT_PUBLIC_SUPABASE_URL}", + "NEXT_PUBLIC_SUPABASE_ANON_KEY": "${localEnv:NEXT_PUBLIC_SUPABASE_ANON_KEY}" + }, + + // Setup-Script (funktioniert in beiden Umgebungen) + "postCreateCommand": "bash ./.devcontainer/setup-repos.sh", + + // Öffnet den Workspace nach dem Verbinden + "postAttachCommand": { + "Open Workspace": "code ./.vscode/workspace.code-workspace" + } +} \ No newline at end of file diff --git a/website-monitoring-devops/.devcontainer/setup-repos.sh b/website-monitoring-devops/.devcontainer/setup-repos.sh new file mode 100644 index 0000000..2133a24 --- /dev/null +++ b/website-monitoring-devops/.devcontainer/setup-repos.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Working directory setup +cd /workspaces + +# Clone repositories if they don't exist +if [ ! -d "/workspaces/frontend" ]; then + echo "Cloning Frontend repository..." + git clone https://github.com/DEINUSERNAME/website-monitoring-frontend /workspaces/frontend +fi + +if [ ! -d "/workspaces/backend" ]; then + echo "Cloning Backend repository..." + git clone https://github.com/DEINUSERNAME/website-monitoring-backend /workspaces/backend +fi + +# Install dependencies +echo "Installing frontend dependencies..." +cd /workspaces/frontend +npm install + +echo "Installing backend dependencies..." +cd /workspaces/backend +npm install + +echo "Workspace setup complete!" \ No newline at end of file diff --git a/website-monitoring-devops/docker-compose.yml b/website-monitoring-devops/docker-compose.yml new file mode 100644 index 0000000..bf145a2 --- /dev/null +++ b/website-monitoring-devops/docker-compose.yml @@ -0,0 +1,65 @@ +services: + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-monitoring} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-monitoring_pass} + POSTGRES_DB: ${POSTGRES_DB:-monitoring} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-monitoring}"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: + context: ../website-monitoring-backend + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "5000:5000" + depends_on: + db: + condition: service_healthy + environment: + PORT: 5000 + DATABASE_URL: postgresql://${POSTGRES_USER:-monitoring}:${POSTGRES_PASSWORD:-monitoring_pass}@db:5432/${POSTGRES_DB:-monitoring} + CORS_ORIGIN: http://localhost:3000 + CHROME_PATH: /usr/bin/chromium + NODE_ENV: production + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:5000/health || exit 1"] + interval: 15s + timeout: 10s + retries: 3 + start_period: 30s + + frontend: + build: + context: ../website-monitoring-frontend + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "3000:3000" + depends_on: + backend: + condition: service_healthy + environment: + NEXT_PUBLIC_API_URL: http://backend:5000 + NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:-} + NODE_ENV: production + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"] + interval: 15s + timeout: 10s + retries: 3 + start_period: 60s + +volumes: + postgres_data: diff --git a/website-monitoring-frontend/.cursor/mcp.json b/website-monitoring-frontend/.cursor/mcp.json new file mode 100644 index 0000000..6d178ec --- /dev/null +++ b/website-monitoring-frontend/.cursor/mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "task-master-ai": { + "command": "npx", + "args": ["-y", "--package=task-master-ai", "task-master-ai"], + "env": { + "OLLAMA_BASE_URL": "http://localhost:11434/api", + "MODEL": "devstral:latest" + } + } + } +} diff --git a/website-monitoring-frontend/.cursor/rules/cursor_rules.mdc b/website-monitoring-frontend/.cursor/rules/cursor_rules.mdc new file mode 100644 index 0000000..7dfae3d --- /dev/null +++ b/website-monitoring-frontend/.cursor/rules/cursor_rules.mdc @@ -0,0 +1,53 @@ +--- +description: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness. +globs: .cursor/rules/*.mdc +alwaysApply: true +--- + +- **Required Rule Structure:** + ```markdown + --- + description: Clear, one-line description of what the rule enforces + globs: path/to/files/*.ext, other/path/**/* + alwaysApply: boolean + --- + + - **Main Points in Bold** + - Sub-points with details + - Examples and explanations + ``` + +- **File References:** + - Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files + - Example: [prisma.mdc](mdc:.cursor/rules/prisma.mdc) for rule references + - Example: [schema.prisma](mdc:prisma/schema.prisma) for code references + +- **Code Examples:** + - Use language-specific code blocks + ```typescript + // ✅ DO: Show good examples + const goodExample = true; + + // ❌ DON'T: Show anti-patterns + const badExample = false; + ``` + +- **Rule Content Guidelines:** + - Start with high-level overview + - Include specific, actionable requirements + - Show examples of correct implementation + - Reference existing code when possible + - Keep rules DRY by referencing other rules + +- **Rule Maintenance:** + - Update rules when new patterns emerge + - Add examples from actual codebase + - Remove outdated patterns + - Cross-reference related rules + +- **Best Practices:** + - Use bullet points for clarity + - Keep descriptions concise + - Include both DO and DON'T examples + - Reference actual code over theoretical examples + - Use consistent formatting across rules \ No newline at end of file diff --git a/website-monitoring-frontend/.cursor/rules/self_improve.mdc b/website-monitoring-frontend/.cursor/rules/self_improve.mdc new file mode 100644 index 0000000..40b31b6 --- /dev/null +++ b/website-monitoring-frontend/.cursor/rules/self_improve.mdc @@ -0,0 +1,72 @@ +--- +description: Guidelines for continuously improving Cursor rules based on emerging code patterns and best practices. +globs: **/* +alwaysApply: true +--- + +- **Rule Improvement Triggers:** + - New code patterns not covered by existing rules + - Repeated similar implementations across files + - Common error patterns that could be prevented + - New libraries or tools being used consistently + - Emerging best practices in the codebase + +- **Analysis Process:** + - Compare new code with existing rules + - Identify patterns that should be standardized + - Look for references to external documentation + - Check for consistent error handling patterns + - Monitor test patterns and coverage + +- **Rule Updates:** + - **Add New Rules When:** + - A new technology/pattern is used in 3+ files + - Common bugs could be prevented by a rule + - Code reviews repeatedly mention the same feedback + - New security or performance patterns emerge + + - **Modify Existing Rules When:** + - Better examples exist in the codebase + - Additional edge cases are discovered + - Related rules have been updated + - Implementation details have changed + +- **Example Pattern Recognition:** + ```typescript + // If you see repeated patterns like: + const data = await prisma.user.findMany({ + select: { id: true, email: true }, + where: { status: 'ACTIVE' } + }); + + // Consider adding to [prisma.mdc](mdc:.cursor/rules/prisma.mdc): + // - Standard select fields + // - Common where conditions + // - Performance optimization patterns + ``` + +- **Rule Quality Checks:** + - Rules should be actionable and specific + - Examples should come from actual code + - References should be up to date + - Patterns should be consistently enforced + +- **Continuous Improvement:** + - Monitor code review comments + - Track common development questions + - Update rules after major refactors + - Add links to relevant documentation + - Cross-reference related rules + +- **Rule Deprecation:** + - Mark outdated patterns as deprecated + - Remove rules that no longer apply + - Update references to deprecated rules + - Document migration paths for old patterns + +- **Documentation Updates:** + - Keep examples synchronized with code + - Update references to external docs + - Maintain links between related rules + - Document breaking changes +Follow [cursor_rules.mdc](mdc:.cursor/rules/cursor_rules.mdc) for proper rule formatting and structure. diff --git a/website-monitoring-frontend/.cursor/rules/taskmaster/dev_workflow.mdc b/website-monitoring-frontend/.cursor/rules/taskmaster/dev_workflow.mdc new file mode 100644 index 0000000..84dd906 --- /dev/null +++ b/website-monitoring-frontend/.cursor/rules/taskmaster/dev_workflow.mdc @@ -0,0 +1,424 @@ +--- +description: Guide for using Taskmaster to manage task-driven development workflows +globs: **/* +alwaysApply: true +--- + +# Taskmaster Development Workflow + +This guide outlines the standard process for using Taskmaster to manage software development projects. It is written as a set of instructions for you, the AI agent. + +- **Your Default Stance**: For most projects, the user can work directly within the `master` task context. Your initial actions should operate on this default context unless a clear pattern for multi-context work emerges. +- **Your Goal**: Your role is to elevate the user's workflow by intelligently introducing advanced features like **Tagged Task Lists** when you detect the appropriate context. Do not force tags on the user; suggest them as a helpful solution to a specific need. + +## The Basic Loop +The fundamental development cycle you will facilitate is: +1. **`list`**: Show the user what needs to be done. +2. **`next`**: Help the user decide what to work on. +3. **`show `**: Provide details for a specific task. +4. **`expand `**: Break down a complex task into smaller, manageable subtasks. +5. **Implement**: The user writes the code and tests. +6. **`update-subtask`**: Log progress and findings on behalf of the user. +7. **`set-status`**: Mark tasks and subtasks as `done` as work is completed. +8. **Repeat**. + +All your standard command executions should operate on the user's current task context, which defaults to `master`. + +--- + +## Standard Development Workflow Process + +### Simple Workflow (Default Starting Point) + +For new projects or when users are getting started, operate within the `master` tag context: + +- Start new projects by running `initialize_project` tool / `task-master init` or `parse_prd` / `task-master parse-prd --input=''` (see @`taskmaster.mdc`) to generate initial tasks.json with tagged structure +- Configure rule sets during initialization with `--rules` flag (e.g., `task-master init --rules cursor,windsurf`) or manage them later with `task-master rules add/remove` commands +- Begin coding sessions with `get_tasks` / `task-master list` (see @`taskmaster.mdc`) to see current tasks, status, and IDs +- Determine the next task to work on using `next_task` / `task-master next` (see @`taskmaster.mdc`) +- Analyze task complexity with `analyze_project_complexity` / `task-master analyze-complexity --research` (see @`taskmaster.mdc`) before breaking down tasks +- Review complexity report using `complexity_report` / `task-master complexity-report` (see @`taskmaster.mdc`) +- Select tasks based on dependencies (all marked 'done'), priority level, and ID order +- View specific task details using `get_task` / `task-master show ` (see @`taskmaster.mdc`) to understand implementation requirements +- Break down complex tasks using `expand_task` / `task-master expand --id= --force --research` (see @`taskmaster.mdc`) with appropriate flags like `--force` (to replace existing subtasks) and `--research` +- Implement code following task details, dependencies, and project standards +- Mark completed tasks with `set_task_status` / `task-master set-status --id= --status=done` (see @`taskmaster.mdc`) +- Update dependent tasks when implementation differs from original plan using `update` / `task-master update --from= --prompt="..."` or `update_task` / `task-master update-task --id= --prompt="..."` (see @`taskmaster.mdc`) + +--- + +## Leveling Up: Agent-Led Multi-Context Workflows + +While the basic workflow is powerful, your primary opportunity to add value is by identifying when to introduce **Tagged Task Lists**. These patterns are your tools for creating a more organized and efficient development environment for the user, especially if you detect agentic or parallel development happening across the same session. + +**Critical Principle**: Most users should never see a difference in their experience. Only introduce advanced workflows when you detect clear indicators that the project has evolved beyond simple task management. + +### When to Introduce Tags: Your Decision Patterns + +Here are the patterns to look for. When you detect one, you should propose the corresponding workflow to the user. + +#### Pattern 1: Simple Git Feature Branching +This is the most common and direct use case for tags. + +- **Trigger**: The user creates a new git branch (e.g., `git checkout -b feature/user-auth`). +- **Your Action**: Propose creating a new tag that mirrors the branch name to isolate the feature's tasks from `master`. +- **Your Suggested Prompt**: *"I see you've created a new branch named 'feature/user-auth'. To keep all related tasks neatly organized and separate from your main list, I can create a corresponding task tag for you. This helps prevent merge conflicts in your `tasks.json` file later. Shall I create the 'feature-user-auth' tag?"* +- **Tool to Use**: `task-master add-tag --from-branch` + +#### Pattern 2: Team Collaboration +- **Trigger**: The user mentions working with teammates (e.g., "My teammate Alice is handling the database schema," or "I need to review Bob's work on the API."). +- **Your Action**: Suggest creating a separate tag for the user's work to prevent conflicts with shared master context. +- **Your Suggested Prompt**: *"Since you're working with Alice, I can create a separate task context for your work to avoid conflicts. This way, Alice can continue working with the master list while you have your own isolated context. When you're ready to merge your work, we can coordinate the tasks back to master. Shall I create a tag for your current work?"* +- **Tool to Use**: `task-master add-tag my-work --copy-from-current --description="My tasks while collaborating with Alice"` + +#### Pattern 3: Experiments or Risky Refactors +- **Trigger**: The user wants to try something that might not be kept (e.g., "I want to experiment with switching our state management library," or "Let's refactor the old API module, but I want to keep the current tasks as a reference."). +- **Your Action**: Propose creating a sandboxed tag for the experimental work. +- **Your Suggested Prompt**: *"This sounds like a great experiment. To keep these new tasks separate from our main plan, I can create a temporary 'experiment-zustand' tag for this work. If we decide not to proceed, we can simply delete the tag without affecting the main task list. Sound good?"* +- **Tool to Use**: `task-master add-tag experiment-zustand --description="Exploring Zustand migration"` + +#### Pattern 4: Large Feature Initiatives (PRD-Driven) +This is a more structured approach for significant new features or epics. + +- **Trigger**: The user describes a large, multi-step feature that would benefit from a formal plan. +- **Your Action**: Propose a comprehensive, PRD-driven workflow. +- **Your Suggested Prompt**: *"This sounds like a significant new feature. To manage this effectively, I suggest we create a dedicated task context for it. Here's the plan: I'll create a new tag called 'feature-xyz', then we can draft a Product Requirements Document (PRD) together to scope the work. Once the PRD is ready, I'll automatically generate all the necessary tasks within that new tag. How does that sound?"* +- **Your Implementation Flow**: + 1. **Create an empty tag**: `task-master add-tag feature-xyz --description "Tasks for the new XYZ feature"`. You can also start by creating a git branch if applicable, and then create the tag from that branch. + 2. **Collaborate & Create PRD**: Work with the user to create a detailed PRD file (e.g., `.taskmaster/docs/feature-xyz-prd.txt`). + 3. **Parse PRD into the new tag**: `task-master parse-prd .taskmaster/docs/feature-xyz-prd.txt --tag feature-xyz` + 4. **Prepare the new task list**: Follow up by suggesting `analyze-complexity` and `expand-all` for the newly created tasks within the `feature-xyz` tag. + +#### Pattern 5: Version-Based Development +Tailor your approach based on the project maturity indicated by tag names. + +- **Prototype/MVP Tags** (`prototype`, `mvp`, `poc`, `v0.x`): + - **Your Approach**: Focus on speed and functionality over perfection + - **Task Generation**: Create tasks that emphasize "get it working" over "get it perfect" + - **Complexity Level**: Lower complexity, fewer subtasks, more direct implementation paths + - **Research Prompts**: Include context like "This is a prototype - prioritize speed and basic functionality over optimization" + - **Example Prompt Addition**: *"Since this is for the MVP, I'll focus on tasks that get core functionality working quickly rather than over-engineering."* + +- **Production/Mature Tags** (`v1.0+`, `production`, `stable`): + - **Your Approach**: Emphasize robustness, testing, and maintainability + - **Task Generation**: Include comprehensive error handling, testing, documentation, and optimization + - **Complexity Level**: Higher complexity, more detailed subtasks, thorough implementation paths + - **Research Prompts**: Include context like "This is for production - prioritize reliability, performance, and maintainability" + - **Example Prompt Addition**: *"Since this is for production, I'll ensure tasks include proper error handling, testing, and documentation."* + +### Advanced Workflow (Tag-Based & PRD-Driven) + +**When to Transition**: Recognize when the project has evolved (or has initiated a project which existing code) beyond simple task management. Look for these indicators: +- User mentions teammates or collaboration needs +- Project has grown to 15+ tasks with mixed priorities +- User creates feature branches or mentions major initiatives +- User initializes Taskmaster on an existing, complex codebase +- User describes large features that would benefit from dedicated planning + +**Your Role in Transition**: Guide the user to a more sophisticated workflow that leverages tags for organization and PRDs for comprehensive planning. + +#### Master List Strategy (High-Value Focus) +Once you transition to tag-based workflows, the `master` tag should ideally contain only: +- **High-level deliverables** that provide significant business value +- **Major milestones** and epic-level features +- **Critical infrastructure** work that affects the entire project +- **Release-blocking** items + +**What NOT to put in master**: +- Detailed implementation subtasks (these go in feature-specific tags' parent tasks) +- Refactoring work (create dedicated tags like `refactor-auth`) +- Experimental features (use `experiment-*` tags) +- Team member-specific tasks (use person-specific tags) + +#### PRD-Driven Feature Development + +**For New Major Features**: +1. **Identify the Initiative**: When user describes a significant feature +2. **Create Dedicated Tag**: `add_tag feature-[name] --description="[Feature description]"` +3. **Collaborative PRD Creation**: Work with user to create comprehensive PRD in `.taskmaster/docs/feature-[name]-prd.txt` +4. **Parse & Prepare**: + - `parse_prd .taskmaster/docs/feature-[name]-prd.txt --tag=feature-[name]` + - `analyze_project_complexity --tag=feature-[name] --research` + - `expand_all --tag=feature-[name] --research` +5. **Add Master Reference**: Create a high-level task in `master` that references the feature tag + +**For Existing Codebase Analysis**: +When users initialize Taskmaster on existing projects: +1. **Codebase Discovery**: Use your native tools for producing deep context about the code base. You may use `research` tool with `--tree` and `--files` to collect up to date information using the existing architecture as context. +2. **Collaborative Assessment**: Work with user to identify improvement areas, technical debt, or new features +3. **Strategic PRD Creation**: Co-author PRDs that include: + - Current state analysis (based on your codebase research) + - Proposed improvements or new features + - Implementation strategy considering existing code +4. **Tag-Based Organization**: Parse PRDs into appropriate tags (`refactor-api`, `feature-dashboard`, `tech-debt`, etc.) +5. **Master List Curation**: Keep only the most valuable initiatives in master + +The parse-prd's `--append` flag enables the user to parse multiple PRDs within tags or across tags. PRDs should be focused and the number of tasks they are parsed into should be strategically chosen relative to the PRD's complexity and level of detail. + +### Workflow Transition Examples + +**Example 1: Simple → Team-Based** +``` +User: "Alice is going to help with the API work" +Your Response: "Great! To avoid conflicts, I'll create a separate task context for your work. Alice can continue with the master list while you work in your own context. When you're ready to merge, we can coordinate the tasks back together." +Action: add_tag my-api-work --copy-from-current --description="My API tasks while collaborating with Alice" +``` + +**Example 2: Simple → PRD-Driven** +``` +User: "I want to add a complete user dashboard with analytics, user management, and reporting" +Your Response: "This sounds like a major feature that would benefit from detailed planning. Let me create a dedicated context for this work and we can draft a PRD together to ensure we capture all requirements." +Actions: +1. add_tag feature-dashboard --description="User dashboard with analytics and management" +2. Collaborate on PRD creation +3. parse_prd dashboard-prd.txt --tag=feature-dashboard +4. Add high-level "User Dashboard" task to master +``` + +**Example 3: Existing Project → Strategic Planning** +``` +User: "I just initialized Taskmaster on my existing React app. It's getting messy and I want to improve it." +Your Response: "Let me research your codebase to understand the current architecture, then we can create a strategic plan for improvements." +Actions: +1. research "Current React app architecture and improvement opportunities" --tree --files=src/ +2. Collaborate on improvement PRD based on findings +3. Create tags for different improvement areas (refactor-components, improve-state-management, etc.) +4. Keep only major improvement initiatives in master +``` + +--- + +## Primary Interaction: MCP Server vs. CLI + +Taskmaster offers two primary ways to interact: + +1. **MCP Server (Recommended for Integrated Tools)**: + - For AI agents and integrated development environments (like Cursor), interacting via the **MCP server is the preferred method**. + - The MCP server exposes Taskmaster functionality through a set of tools (e.g., `get_tasks`, `add_subtask`). + - This method offers better performance, structured data exchange, and richer error handling compared to CLI parsing. + - Refer to @`mcp.mdc` for details on the MCP architecture and available tools. + - A comprehensive list and description of MCP tools and their corresponding CLI commands can be found in @`taskmaster.mdc`. + - **Restart the MCP server** if core logic in `scripts/modules` or MCP tool/direct function definitions change. + - **Note**: MCP tools fully support tagged task lists with complete tag management capabilities. + +2. **`task-master` CLI (For Users & Fallback)**: + - The global `task-master` command provides a user-friendly interface for direct terminal interaction. + - It can also serve as a fallback if the MCP server is inaccessible or a specific function isn't exposed via MCP. + - Install globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`. + - The CLI commands often mirror the MCP tools (e.g., `task-master list` corresponds to `get_tasks`). + - Refer to @`taskmaster.mdc` for a detailed command reference. + - **Tagged Task Lists**: CLI fully supports the new tagged system with seamless migration. + +## How the Tag System Works (For Your Reference) + +- **Data Structure**: Tasks are organized into separate contexts (tags) like "master", "feature-branch", or "v2.0". +- **Silent Migration**: Existing projects automatically migrate to use a "master" tag with zero disruption. +- **Context Isolation**: Tasks in different tags are completely separate. Changes in one tag do not affect any other tag. +- **Manual Control**: The user is always in control. There is no automatic switching. You facilitate switching by using `use-tag `. +- **Full CLI & MCP Support**: All tag management commands are available through both the CLI and MCP tools for you to use. Refer to @`taskmaster.mdc` for a full command list. + +--- + +## Task Complexity Analysis + +- Run `analyze_project_complexity` / `task-master analyze-complexity --research` (see @`taskmaster.mdc`) for comprehensive analysis +- Review complexity report via `complexity_report` / `task-master complexity-report` (see @`taskmaster.mdc`) for a formatted, readable version. +- Focus on tasks with highest complexity scores (8-10) for detailed breakdown +- Use analysis results to determine appropriate subtask allocation +- Note that reports are automatically used by the `expand_task` tool/command + +## Task Breakdown Process + +- Use `expand_task` / `task-master expand --id=`. It automatically uses the complexity report if found, otherwise generates default number of subtasks. +- Use `--num=` to specify an explicit number of subtasks, overriding defaults or complexity report recommendations. +- Add `--research` flag to leverage Perplexity AI for research-backed expansion. +- Add `--force` flag to clear existing subtasks before generating new ones (default is to append). +- Use `--prompt=""` to provide additional context when needed. +- Review and adjust generated subtasks as necessary. +- Use `expand_all` tool or `task-master expand --all` to expand multiple pending tasks at once, respecting flags like `--force` and `--research`. +- If subtasks need complete replacement (regardless of the `--force` flag on `expand`), clear them first with `clear_subtasks` / `task-master clear-subtasks --id=`. + +## Implementation Drift Handling + +- When implementation differs significantly from planned approach +- When future tasks need modification due to current implementation choices +- When new dependencies or requirements emerge +- Use `update` / `task-master update --from= --prompt='\nUpdate context...' --research` to update multiple future tasks. +- Use `update_task` / `task-master update-task --id= --prompt='\nUpdate context...' --research` to update a single specific task. + +## Task Status Management + +- Use 'pending' for tasks ready to be worked on +- Use 'done' for completed and verified tasks +- Use 'deferred' for postponed tasks +- Add custom status values as needed for project-specific workflows + +## Task Structure Fields + +- **id**: Unique identifier for the task (Example: `1`, `1.1`) +- **title**: Brief, descriptive title (Example: `"Initialize Repo"`) +- **description**: Concise summary of what the task involves (Example: `"Create a new repository, set up initial structure."`) +- **status**: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`) +- **dependencies**: IDs of prerequisite tasks (Example: `[1, 2.1]`) + - Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending) + - This helps quickly identify which prerequisite tasks are blocking work +- **priority**: Importance level (Example: `"high"`, `"medium"`, `"low"`) +- **details**: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`) +- **testStrategy**: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`) +- **subtasks**: List of smaller, more specific tasks (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`) +- Refer to task structure details (previously linked to `tasks.mdc`). + +## Configuration Management (Updated) + +Taskmaster configuration is managed through two main mechanisms: + +1. **`.taskmaster/config.json` File (Primary):** + * Located in the project root directory. + * Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc. + * **Tagged System Settings**: Includes `global.defaultTag` (defaults to "master") and `tags` section for tag management configuration. + * **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing. + * **View/Set specific models via `task-master models` command or `models` MCP tool.** + * Created automatically when you run `task-master models --setup` for the first time or during tagged system migration. + +2. **Environment Variables (`.env` / `mcp.json`):** + * Used **only** for sensitive API keys and specific endpoint URLs. + * Place API keys (one per provider) in a `.env` file in the project root for CLI usage. + * For MCP/Cursor integration, configure these keys in the `env` section of `.cursor/mcp.json`. + * Available keys/variables: See `assets/env.example` or the Configuration section in the command reference (previously linked to `taskmaster.mdc`). + +3. **`.taskmaster/state.json` File (Tagged System State):** + * Tracks current tag context and migration status. + * Automatically created during tagged system migration. + * Contains: `currentTag`, `lastSwitched`, `migrationNoticeShown`. + +**Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `TASKMASTER_LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool. +**If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the `env` section of `.cursor/mcp.json`. +**If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the `.env` file in the root of the project. + +## Rules Management + +Taskmaster supports multiple AI coding assistant rule sets that can be configured during project initialization or managed afterward: + +- **Available Profiles**: Claude Code, Cline, Codex, Cursor, Roo Code, Trae, Windsurf (claude, cline, codex, cursor, roo, trae, windsurf) +- **During Initialization**: Use `task-master init --rules cursor,windsurf` to specify which rule sets to include +- **After Initialization**: Use `task-master rules add ` or `task-master rules remove ` to manage rule sets +- **Interactive Setup**: Use `task-master rules setup` to launch an interactive prompt for selecting rule profiles +- **Default Behavior**: If no `--rules` flag is specified during initialization, all available rule profiles are included +- **Rule Structure**: Each profile creates its own directory (e.g., `.cursor/rules`, `.roo/rules`) with appropriate configuration files + +## Determining the Next Task + +- Run `next_task` / `task-master next` to show the next task to work on. +- The command identifies tasks with all dependencies satisfied +- Tasks are prioritized by priority level, dependency count, and ID +- The command shows comprehensive task information including: + - Basic task details and description + - Implementation details + - Subtasks (if they exist) + - Contextual suggested actions +- Recommended before starting any new development work +- Respects your project's dependency structure +- Ensures tasks are completed in the appropriate sequence +- Provides ready-to-use commands for common task actions + +## Viewing Specific Task Details + +- Run `get_task` / `task-master show ` to view a specific task. +- Use dot notation for subtasks: `task-master show 1.2` (shows subtask 2 of task 1) +- Displays comprehensive information similar to the next command, but for a specific task +- For parent tasks, shows all subtasks and their current status +- For subtasks, shows parent task information and relationship +- Provides contextual suggested actions appropriate for the specific task +- Useful for examining task details before implementation or checking status + +## Managing Task Dependencies + +- Use `add_dependency` / `task-master add-dependency --id= --depends-on=` to add a dependency. +- Use `remove_dependency` / `task-master remove-dependency --id= --depends-on=` to remove a dependency. +- The system prevents circular dependencies and duplicate dependency entries +- Dependencies are checked for existence before being added or removed +- Task files are automatically regenerated after dependency changes +- Dependencies are visualized with status indicators in task listings and files + +## Task Reorganization + +- Use `move_task` / `task-master move --from= --to=` to move tasks or subtasks within the hierarchy +- This command supports several use cases: + - Moving a standalone task to become a subtask (e.g., `--from=5 --to=7`) + - Moving a subtask to become a standalone task (e.g., `--from=5.2 --to=7`) + - Moving a subtask to a different parent (e.g., `--from=5.2 --to=7.3`) + - Reordering subtasks within the same parent (e.g., `--from=5.2 --to=5.4`) + - Moving a task to a new, non-existent ID position (e.g., `--from=5 --to=25`) + - Moving multiple tasks at once using comma-separated IDs (e.g., `--from=10,11,12 --to=16,17,18`) +- The system includes validation to prevent data loss: + - Allows moving to non-existent IDs by creating placeholder tasks + - Prevents moving to existing task IDs that have content (to avoid overwriting) + - Validates source tasks exist before attempting to move them +- The system maintains proper parent-child relationships and dependency integrity +- Task files are automatically regenerated after the move operation +- This provides greater flexibility in organizing and refining your task structure as project understanding evolves +- This is especially useful when dealing with potential merge conflicts arising from teams creating tasks on separate branches. Solve these conflicts very easily by moving your tasks and keeping theirs. + +## Iterative Subtask Implementation + +Once a task has been broken down into subtasks using `expand_task` or similar methods, follow this iterative process for implementation: + +1. **Understand the Goal (Preparation):** + * Use `get_task` / `task-master show ` (see @`taskmaster.mdc`) to thoroughly understand the specific goals and requirements of the subtask. + +2. **Initial Exploration & Planning (Iteration 1):** + * This is the first attempt at creating a concrete implementation plan. + * Explore the codebase to identify the precise files, functions, and even specific lines of code that will need modification. + * Determine the intended code changes (diffs) and their locations. + * Gather *all* relevant details from this exploration phase. + +3. **Log the Plan:** + * Run `update_subtask` / `task-master update-subtask --id= --prompt=''`. + * Provide the *complete and detailed* findings from the exploration phase in the prompt. Include file paths, line numbers, proposed diffs, reasoning, and any potential challenges identified. Do not omit details. The goal is to create a rich, timestamped log within the subtask's `details`. + +4. **Verify the Plan:** + * Run `get_task` / `task-master show ` again to confirm that the detailed implementation plan has been successfully appended to the subtask's details. + +5. **Begin Implementation:** + * Set the subtask status using `set_task_status` / `task-master set-status --id= --status=in-progress`. + * Start coding based on the logged plan. + +6. **Refine and Log Progress (Iteration 2+):** + * As implementation progresses, you will encounter challenges, discover nuances, or confirm successful approaches. + * **Before appending new information**: Briefly review the *existing* details logged in the subtask (using `get_task` or recalling from context) to ensure the update adds fresh insights and avoids redundancy. + * **Regularly** use `update_subtask` / `task-master update-subtask --id= --prompt='\n- What worked...\n- What didn't work...'` to append new findings. + * **Crucially, log:** + * What worked ("fundamental truths" discovered). + * What didn't work and why (to avoid repeating mistakes). + * Specific code snippets or configurations that were successful. + * Decisions made, especially if confirmed with user input. + * Any deviations from the initial plan and the reasoning. + * The objective is to continuously enrich the subtask's details, creating a log of the implementation journey that helps the AI (and human developers) learn, adapt, and avoid repeating errors. + +7. **Review & Update Rules (Post-Implementation):** + * Once the implementation for the subtask is functionally complete, review all code changes and the relevant chat history. + * Identify any new or modified code patterns, conventions, or best practices established during the implementation. + * Create new or update existing rules following internal guidelines (previously linked to `cursor_rules.mdc` and `self_improve.mdc`). + +8. **Mark Task Complete:** + * After verifying the implementation and updating any necessary rules, mark the subtask as completed: `set_task_status` / `task-master set-status --id= --status=done`. + +9. **Commit Changes (If using Git):** + * Stage the relevant code changes and any updated/new rule files (`git add .`). + * Craft a comprehensive Git commit message summarizing the work done for the subtask, including both code implementation and any rule adjustments. + * Execute the commit command directly in the terminal (e.g., `git commit -m 'feat(module): Implement feature X for subtask \n\n- Details about changes...\n- Updated rule Y for pattern Z'`). + * Consider if a Changeset is needed according to internal versioning guidelines (previously linked to `changeset.mdc`). If so, run `npm run changeset`, stage the generated file, and amend the commit or create a new one. + +10. **Proceed to Next Subtask:** + * Identify the next subtask (e.g., using `next_task` / `task-master next`). + +## Code Analysis & Refactoring Techniques + +- **Top-Level Function Search**: + - Useful for understanding module structure or planning refactors. + - Use grep/ripgrep to find exported functions/constants: + `rg "export (async function|function|const) \w+"` or similar patterns. + - Can help compare functions between files during migrations or identify potential naming conflicts. + +--- +*This workflow provides a general guideline. Adapt it based on your specific project needs and team practices.* \ No newline at end of file diff --git a/website-monitoring-frontend/.cursor/rules/taskmaster/taskmaster.mdc b/website-monitoring-frontend/.cursor/rules/taskmaster/taskmaster.mdc new file mode 100644 index 0000000..e9be2ae --- /dev/null +++ b/website-monitoring-frontend/.cursor/rules/taskmaster/taskmaster.mdc @@ -0,0 +1,558 @@ +--- +description: Comprehensive reference for Taskmaster MCP tools and CLI commands. +globs: **/* +alwaysApply: true +--- + +# Taskmaster Tool & Command Reference + +This document provides a detailed reference for interacting with Taskmaster, covering both the recommended MCP tools, suitable for integrations like Cursor, and the corresponding `task-master` CLI commands, designed for direct user interaction or fallback. + +**Note:** For interacting with Taskmaster programmatically or via integrated tools, using the **MCP tools is strongly recommended** due to better performance, structured data, and error handling. The CLI commands serve as a user-friendly alternative and fallback. + +**Important:** Several MCP tools involve AI processing... The AI-powered tools include `parse_prd`, `analyze_project_complexity`, `update_subtask`, `update_task`, `update`, `expand_all`, `expand_task`, and `add_task`. + +**🏷️ Tagged Task Lists System:** Task Master now supports **tagged task lists** for multi-context task management. This allows you to maintain separate, isolated lists of tasks for different features, branches, or experiments. Existing projects are seamlessly migrated to use a default "master" tag. Most commands now support a `--tag ` flag to specify which context to operate on. If omitted, commands use the currently active tag. + +--- + +## Initialization & Setup + +### 1. Initialize Project (`init`) + +* **MCP Tool:** `initialize_project` +* **CLI Command:** `task-master init [options]` +* **Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project.` +* **Key CLI Options:** + * `--name `: `Set the name for your project in Taskmaster's configuration.` + * `--description `: `Provide a brief description for your project.` + * `--version `: `Set the initial version for your project, e.g., '0.1.0'.` + * `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.` +* **Usage:** Run this once at the beginning of a new project. +* **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.` +* **Key MCP Parameters/Options:** + * `projectName`: `Set the name for your project.` (CLI: `--name `) + * `projectDescription`: `Provide a brief description for your project.` (CLI: `--description `) + * `projectVersion`: `Set the initial version for your project, e.g., '0.1.0'.` (CLI: `--version `) + * `authorName`: `Author name.` (CLI: `--author `) + * `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`) + * `addAliases`: `Add shell aliases tm and taskmaster. Default is false.` (CLI: `--aliases`) + * `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`) +* **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like Cursor. Operates on the current working directory of the MCP server. +* **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in .taskmaster/templates/example_prd.txt. +* **Tagging:** Use the `--tag` option to parse the PRD into a specific, non-default tag context. If the tag doesn't exist, it will be created automatically. Example: `task-master parse-prd spec.txt --tag=new-feature`. + +### 2. Parse PRD (`parse_prd`) + +* **MCP Tool:** `parse_prd` +* **CLI Command:** `task-master parse-prd [file] [options]` +* **Description:** `Parse a Product Requirements Document, PRD, or text file with Taskmaster to automatically generate an initial set of tasks in tasks.json.` +* **Key Parameters/Options:** + * `input`: `Path to your PRD or requirements text file that Taskmaster should parse for tasks.` (CLI: `[file]` positional or `-i, --input `) + * `output`: `Specify where Taskmaster should save the generated 'tasks.json' file. Defaults to '.taskmaster/tasks/tasks.json'.` (CLI: `-o, --output `) + * `numTasks`: `Approximate number of top-level tasks Taskmaster should aim to generate from the document.` (CLI: `-n, --num-tasks `) + * `force`: `Use this to allow Taskmaster to overwrite an existing 'tasks.json' without asking for confirmation.` (CLI: `-f, --force`) +* **Usage:** Useful for bootstrapping a project from an existing requirements document. +* **Notes:** Task Master will strictly adhere to any specific requirements mentioned in the PRD, such as libraries, database schemas, frameworks, tech stacks, etc., while filling in any gaps where the PRD isn't fully specified. Tasks are designed to provide the most direct implementation path while avoiding over-engineering. +* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. If the user does not have a PRD, suggest discussing their idea and then use the example PRD in `.taskmaster/templates/example_prd.txt` as a template for creating the PRD based on their idea, for use with `parse-prd`. + +--- + +## AI Model Configuration + +### 2. Manage Models (`models`) +* **MCP Tool:** `models` +* **CLI Command:** `task-master models [options]` +* **Description:** `View the current AI model configuration or set specific models for different roles (main, research, fallback). Allows setting custom model IDs for Ollama and OpenRouter.` +* **Key MCP Parameters/Options:** + * `setMain `: `Set the primary model ID for task generation/updates.` (CLI: `--set-main `) + * `setResearch `: `Set the model ID for research-backed operations.` (CLI: `--set-research `) + * `setFallback `: `Set the model ID to use if the primary fails.` (CLI: `--set-fallback `) + * `ollama `: `Indicates the set model ID is a custom Ollama model.` (CLI: `--ollama`) + * `openrouter `: `Indicates the set model ID is a custom OpenRouter model.` (CLI: `--openrouter`) + * `listAvailableModels `: `If true, lists available models not currently assigned to a role.` (CLI: No direct equivalent; CLI lists available automatically) + * `projectRoot `: `Optional. Absolute path to the project root directory.` (CLI: Determined automatically) +* **Key CLI Options:** + * `--set-main `: `Set the primary model.` + * `--set-research `: `Set the research model.` + * `--set-fallback `: `Set the fallback model.` + * `--ollama`: `Specify that the provided model ID is for Ollama (use with --set-*).` + * `--openrouter`: `Specify that the provided model ID is for OpenRouter (use with --set-*). Validates against OpenRouter API.` + * `--bedrock`: `Specify that the provided model ID is for AWS Bedrock (use with --set-*).` + * `--setup`: `Run interactive setup to configure models, including custom Ollama/OpenRouter IDs.` +* **Usage (MCP):** Call without set flags to get current config. Use `setMain`, `setResearch`, or `setFallback` with a valid model ID to update the configuration. Use `listAvailableModels: true` to get a list of unassigned models. To set a custom model, provide the model ID and set `ollama: true` or `openrouter: true`. +* **Usage (CLI):** Run without flags to view current configuration and available models. Use set flags to update specific roles. Use `--setup` for guided configuration, including custom models. To set a custom model via flags, use `--set-=` along with either `--ollama` or `--openrouter`. +* **Notes:** Configuration is stored in `.taskmaster/config.json` in the project root. This command/tool modifies that file. Use `listAvailableModels` or `task-master models` to see internally supported models. OpenRouter custom models are validated against their live API. Ollama custom models are not validated live. +* **API note:** API keys for selected AI providers (based on their model) need to exist in the mcp.json file to be accessible in MCP context. The API keys must be present in the local .env file for the CLI to be able to read them. +* **Model costs:** The costs in supported models are expressed in dollars. An input/output value of 3 is $3.00. A value of 0.8 is $0.80. +* **Warning:** DO NOT MANUALLY EDIT THE .taskmaster/config.json FILE. Use the included commands either in the MCP or CLI format as needed. Always prioritize MCP tools when available and use the CLI as a fallback. + +--- + +## Task Listing & Viewing + +### 3. Get Tasks (`get_tasks`) + +* **MCP Tool:** `get_tasks` +* **CLI Command:** `task-master list [options]` +* **Description:** `List your Taskmaster tasks, optionally filtering by status and showing subtasks.` +* **Key Parameters/Options:** + * `status`: `Show only Taskmaster tasks matching this status (or multiple statuses, comma-separated), e.g., 'pending' or 'done,in-progress'.` (CLI: `-s, --status `) + * `withSubtasks`: `Include subtasks indented under their parent tasks in the list.` (CLI: `--with-subtasks`) + * `tag`: `Specify which tag context to list tasks from. Defaults to the current active tag.` (CLI: `--tag `) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file `) +* **Usage:** Get an overview of the project status, often used at the start of a work session. + +### 4. Get Next Task (`next_task`) + +* **MCP Tool:** `next_task` +* **CLI Command:** `task-master next [options]` +* **Description:** `Ask Taskmaster to show the next available task you can work on, based on status and completed dependencies.` +* **Key Parameters/Options:** + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file `) + * `tag`: `Specify which tag context to use. Defaults to the current active tag.` (CLI: `--tag `) +* **Usage:** Identify what to work on next according to the plan. + +### 5. Get Task Details (`get_task`) + +* **MCP Tool:** `get_task` +* **CLI Command:** `task-master show [id] [options]` +* **Description:** `Display detailed information for one or more specific Taskmaster tasks or subtasks by ID.` +* **Key Parameters/Options:** + * `id`: `Required. The ID of the Taskmaster task (e.g., '15'), subtask (e.g., '15.2'), or a comma-separated list of IDs ('1,5,10.2') you want to view.` (CLI: `[id]` positional or `-i, --id `) + * `tag`: `Specify which tag context to get the task(s) from. Defaults to the current active tag.` (CLI: `--tag `) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file `) +* **Usage:** Understand the full details for a specific task. When multiple IDs are provided, a summary table is shown. +* **CRITICAL INFORMATION** If you need to collect information from multiple tasks, use comma-separated IDs (i.e. 1,2,3) to receive an array of tasks. Do not needlessly get tasks one at a time if you need to get many as that is wasteful. + +--- + +## Task Creation & Modification + +### 6. Add Task (`add_task`) + +* **MCP Tool:** `add_task` +* **CLI Command:** `task-master add-task [options]` +* **Description:** `Add a new task to Taskmaster by describing it; AI will structure it.` +* **Key Parameters/Options:** + * `prompt`: `Required. Describe the new task you want Taskmaster to create, e.g., "Implement user authentication using JWT".` (CLI: `-p, --prompt `) + * `dependencies`: `Specify the IDs of any Taskmaster tasks that must be completed before this new one can start, e.g., '12,14'.` (CLI: `-d, --dependencies `) + * `priority`: `Set the priority for the new task: 'high', 'medium', or 'low'. Default is 'medium'.` (CLI: `--priority `) + * `research`: `Enable Taskmaster to use the research role for potentially more informed task creation.` (CLI: `-r, --research`) + * `tag`: `Specify which tag context to add the task to. Defaults to the current active tag.` (CLI: `--tag `) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file `) +* **Usage:** Quickly add newly identified tasks during development. +* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. + +### 7. Add Subtask (`add_subtask`) + +* **MCP Tool:** `add_subtask` +* **CLI Command:** `task-master add-subtask [options]` +* **Description:** `Add a new subtask to a Taskmaster parent task, or convert an existing task into a subtask.` +* **Key Parameters/Options:** + * `id` / `parent`: `Required. The ID of the Taskmaster task that will be the parent.` (MCP: `id`, CLI: `-p, --parent `) + * `taskId`: `Use this if you want to convert an existing top-level Taskmaster task into a subtask of the specified parent.` (CLI: `-i, --task-id `) + * `title`: `Required if not using taskId. The title for the new subtask Taskmaster should create.` (CLI: `-t, --title `) + * `description`: `A brief description for the new subtask.` (CLI: `-d, --description <text>`) + * `details`: `Provide implementation notes or details for the new subtask.` (CLI: `--details <text>`) + * `dependencies`: `Specify IDs of other tasks or subtasks, e.g., '15' or '16.1', that must be done before this new subtask.` (CLI: `--dependencies <ids>`) + * `status`: `Set the initial status for the new subtask. Default is 'pending'.` (CLI: `-s, --status <status>`) + * `generate`: `Enable Taskmaster to regenerate markdown task files after adding the subtask.` (CLI: `--generate`) + * `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Break down tasks manually or reorganize existing tasks. + +### 8. Update Tasks (`update`) + +* **MCP Tool:** `update` +* **CLI Command:** `task-master update [options]` +* **Description:** `Update multiple upcoming tasks in Taskmaster based on new context or changes, starting from a specific task ID.` +* **Key Parameters/Options:** + * `from`: `Required. The ID of the first task Taskmaster should update. All tasks with this ID or higher that are not 'done' will be considered.` (CLI: `--from <id>`) + * `prompt`: `Required. Explain the change or new context for Taskmaster to apply to the tasks, e.g., "We are now using React Query instead of Redux Toolkit for data fetching".` (CLI: `-p, --prompt <text>`) + * `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`) + * `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Handle significant implementation changes or pivots that affect multiple future tasks. Example CLI: `task-master update --from='18' --prompt='Switching to React Query.\nNeed to refactor data fetching...'` +* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. + +### 9. Update Task (`update_task`) + +* **MCP Tool:** `update_task` +* **CLI Command:** `task-master update-task [options]` +* **Description:** `Modify a specific Taskmaster task by ID, incorporating new information or changes. By default, this replaces the existing task details.` +* **Key Parameters/Options:** + * `id`: `Required. The specific ID of the Taskmaster task, e.g., '15', you want to update.` (CLI: `-i, --id <id>`) + * `prompt`: `Required. Explain the specific changes or provide the new information Taskmaster should incorporate into this task.` (CLI: `-p, --prompt <text>`) + * `append`: `If true, appends the prompt content to the task's details with a timestamp, rather than replacing them. Behaves like update-subtask.` (CLI: `--append`) + * `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`) + * `tag`: `Specify which tag context the task belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Refine a specific task based on new understanding. Use `--append` to log progress without creating subtasks. +* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. + +### 10. Update Subtask (`update_subtask`) + +* **MCP Tool:** `update_subtask` +* **CLI Command:** `task-master update-subtask [options]` +* **Description:** `Append timestamped notes or details to a specific Taskmaster subtask without overwriting existing content. Intended for iterative implementation logging.` +* **Key Parameters/Options:** + * `id`: `Required. The ID of the Taskmaster subtask, e.g., '5.2', to update with new information.` (CLI: `-i, --id <id>`) + * `prompt`: `Required. The information, findings, or progress notes to append to the subtask's details with a timestamp.` (CLI: `-p, --prompt <text>`) + * `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`) + * `tag`: `Specify which tag context the subtask belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Log implementation progress, findings, and discoveries during subtask development. Each update is timestamped and appended to preserve the implementation journey. +* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. + +### 11. Set Task Status (`set_task_status`) + +* **MCP Tool:** `set_task_status` +* **CLI Command:** `task-master set-status [options]` +* **Description:** `Update the status of one or more Taskmaster tasks or subtasks, e.g., 'pending', 'in-progress', 'done'.` +* **Key Parameters/Options:** + * `id`: `Required. The ID(s) of the Taskmaster task(s) or subtask(s), e.g., '15', '15.2', or '16,17.1', to update.` (CLI: `-i, --id <id>`) + * `status`: `Required. The new status to set, e.g., 'done', 'pending', 'in-progress', 'review', 'cancelled'.` (CLI: `-s, --status <status>`) + * `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Mark progress as tasks move through the development cycle. + +### 12. Remove Task (`remove_task`) + +* **MCP Tool:** `remove_task` +* **CLI Command:** `task-master remove-task [options]` +* **Description:** `Permanently remove a task or subtask from the Taskmaster tasks list.` +* **Key Parameters/Options:** + * `id`: `Required. The ID of the Taskmaster task, e.g., '5', or subtask, e.g., '5.2', to permanently remove.` (CLI: `-i, --id <id>`) + * `yes`: `Skip the confirmation prompt and immediately delete the task.` (CLI: `-y, --yes`) + * `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Permanently delete tasks or subtasks that are no longer needed in the project. +* **Notes:** Use with caution as this operation cannot be undone. Consider using 'blocked', 'cancelled', or 'deferred' status instead if you just want to exclude a task from active planning but keep it for reference. The command automatically cleans up dependency references in other tasks. + +--- + +## Task Structure & Breakdown + +### 13. Expand Task (`expand_task`) + +* **MCP Tool:** `expand_task` +* **CLI Command:** `task-master expand [options]` +* **Description:** `Use Taskmaster's AI to break down a complex task into smaller, manageable subtasks. Appends subtasks by default.` +* **Key Parameters/Options:** + * `id`: `The ID of the specific Taskmaster task you want to break down into subtasks.` (CLI: `-i, --id <id>`) + * `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create. Uses complexity analysis/defaults otherwise.` (CLI: `-n, --num <number>`) + * `research`: `Enable Taskmaster to use the research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`) + * `prompt`: `Optional: Provide extra context or specific instructions to Taskmaster for generating the subtasks.` (CLI: `-p, --prompt <text>`) + * `force`: `Optional: If true, clear existing subtasks before generating new ones. Default is false (append).` (CLI: `--force`) + * `tag`: `Specify which tag context the task belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Generate a detailed implementation plan for a complex task before starting coding. Automatically uses complexity report recommendations if available and `num` is not specified. +* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. + +### 14. Expand All Tasks (`expand_all`) + +* **MCP Tool:** `expand_all` +* **CLI Command:** `task-master expand --all [options]` (Note: CLI uses the `expand` command with the `--all` flag) +* **Description:** `Tell Taskmaster to automatically expand all eligible pending/in-progress tasks based on complexity analysis or defaults. Appends subtasks by default.` +* **Key Parameters/Options:** + * `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create per task.` (CLI: `-n, --num <number>`) + * `research`: `Enable research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`) + * `prompt`: `Optional: Provide extra context for Taskmaster to apply generally during expansion.` (CLI: `-p, --prompt <text>`) + * `force`: `Optional: If true, clear existing subtasks before generating new ones for each eligible task. Default is false (append).` (CLI: `--force`) + * `tag`: `Specify which tag context to expand. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Useful after initial task generation or complexity analysis to break down multiple tasks at once. +* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. + +### 15. Clear Subtasks (`clear_subtasks`) + +* **MCP Tool:** `clear_subtasks` +* **CLI Command:** `task-master clear-subtasks [options]` +* **Description:** `Remove all subtasks from one or more specified Taskmaster parent tasks.` +* **Key Parameters/Options:** + * `id`: `The ID(s) of the Taskmaster parent task(s) whose subtasks you want to remove, e.g., '15' or '16,18'. Required unless using 'all'.` (CLI: `-i, --id <ids>`) + * `all`: `Tell Taskmaster to remove subtasks from all parent tasks.` (CLI: `--all`) + * `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Used before regenerating subtasks with `expand_task` if the previous breakdown needs replacement. + +### 16. Remove Subtask (`remove_subtask`) + +* **MCP Tool:** `remove_subtask` +* **CLI Command:** `task-master remove-subtask [options]` +* **Description:** `Remove a subtask from its Taskmaster parent, optionally converting it into a standalone task.` +* **Key Parameters/Options:** + * `id`: `Required. The ID(s) of the Taskmaster subtask(s) to remove, e.g., '15.2' or '16.1,16.3'.` (CLI: `-i, --id <id>`) + * `convert`: `If used, Taskmaster will turn the subtask into a regular top-level task instead of deleting it.` (CLI: `-c, --convert`) + * `generate`: `Enable Taskmaster to regenerate markdown task files after removing the subtask.` (CLI: `--generate`) + * `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Delete unnecessary subtasks or promote a subtask to a top-level task. + +### 17. Move Task (`move_task`) + +* **MCP Tool:** `move_task` +* **CLI Command:** `task-master move [options]` +* **Description:** `Move a task or subtask to a new position within the task hierarchy.` +* **Key Parameters/Options:** + * `from`: `Required. ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated for multiple tasks.` (CLI: `--from <id>`) + * `to`: `Required. ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated.` (CLI: `--to <id>`) + * `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Reorganize tasks by moving them within the hierarchy. Supports various scenarios like: + * Moving a task to become a subtask + * Moving a subtask to become a standalone task + * Moving a subtask to a different parent + * Reordering subtasks within the same parent + * Moving a task to a new, non-existent ID (automatically creates placeholders) + * Moving multiple tasks at once with comma-separated IDs +* **Validation Features:** + * Allows moving tasks to non-existent destination IDs (creates placeholder tasks) + * Prevents moving to existing task IDs that already have content (to avoid overwriting) + * Validates that source tasks exist before attempting to move them + * Maintains proper parent-child relationships +* **Example CLI:** `task-master move --from=5.2 --to=7.3` to move subtask 5.2 to become subtask 7.3. +* **Example Multi-Move:** `task-master move --from=10,11,12 --to=16,17,18` to move multiple tasks to new positions. +* **Common Use:** Resolving merge conflicts in tasks.json when multiple team members create tasks on different branches. + +--- + +## Dependency Management + +### 18. Add Dependency (`add_dependency`) + +* **MCP Tool:** `add_dependency` +* **CLI Command:** `task-master add-dependency [options]` +* **Description:** `Define a dependency in Taskmaster, making one task a prerequisite for another.` +* **Key Parameters/Options:** + * `id`: `Required. The ID of the Taskmaster task that will depend on another.` (CLI: `-i, --id <id>`) + * `dependsOn`: `Required. The ID of the Taskmaster task that must be completed first, the prerequisite.` (CLI: `-d, --depends-on <id>`) + * `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <path>`) +* **Usage:** Establish the correct order of execution between tasks. + +### 19. Remove Dependency (`remove_dependency`) + +* **MCP Tool:** `remove_dependency` +* **CLI Command:** `task-master remove-dependency [options]` +* **Description:** `Remove a dependency relationship between two Taskmaster tasks.` +* **Key Parameters/Options:** + * `id`: `Required. The ID of the Taskmaster task you want to remove a prerequisite from.` (CLI: `-i, --id <id>`) + * `dependsOn`: `Required. The ID of the Taskmaster task that should no longer be a prerequisite.` (CLI: `-d, --depends-on <id>`) + * `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Update task relationships when the order of execution changes. + +### 20. Validate Dependencies (`validate_dependencies`) + +* **MCP Tool:** `validate_dependencies` +* **CLI Command:** `task-master validate-dependencies [options]` +* **Description:** `Check your Taskmaster tasks for dependency issues (like circular references or links to non-existent tasks) without making changes.` +* **Key Parameters/Options:** + * `tag`: `Specify which tag context to validate. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Audit the integrity of your task dependencies. + +### 21. Fix Dependencies (`fix_dependencies`) + +* **MCP Tool:** `fix_dependencies` +* **CLI Command:** `task-master fix-dependencies [options]` +* **Description:** `Automatically fix dependency issues (like circular references or links to non-existent tasks) in your Taskmaster tasks.` +* **Key Parameters/Options:** + * `tag`: `Specify which tag context to fix dependencies in. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Clean up dependency errors automatically. + +--- + +## Analysis & Reporting + +### 22. Analyze Project Complexity (`analyze_project_complexity`) + +* **MCP Tool:** `analyze_project_complexity` +* **CLI Command:** `task-master analyze-complexity [options]` +* **Description:** `Have Taskmaster analyze your tasks to determine their complexity and suggest which ones need to be broken down further.` +* **Key Parameters/Options:** + * `output`: `Where to save the complexity analysis report. Default is '.taskmaster/reports/task-complexity-report.json' (or '..._tagname.json' if a tag is used).` (CLI: `-o, --output <file>`) + * `threshold`: `The minimum complexity score (1-10) that should trigger a recommendation to expand a task.` (CLI: `-t, --threshold <number>`) + * `research`: `Enable research role for more accurate complexity analysis. Requires appropriate API key.` (CLI: `-r, --research`) + * `tag`: `Specify which tag context to analyze. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Used before breaking down tasks to identify which ones need the most attention. +* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. + +### 23. View Complexity Report (`complexity_report`) + +* **MCP Tool:** `complexity_report` +* **CLI Command:** `task-master complexity-report [options]` +* **Description:** `Display the task complexity analysis report in a readable format.` +* **Key Parameters/Options:** + * `tag`: `Specify which tag context to show the report for. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to the complexity report (default: '.taskmaster/reports/task-complexity-report.json').` (CLI: `-f, --file <file>`) +* **Usage:** Review and understand the complexity analysis results after running analyze-complexity. + +--- + +## File Management + +### 24. Generate Task Files (`generate`) + +* **MCP Tool:** `generate` +* **CLI Command:** `task-master generate [options]` +* **Description:** `Create or update individual Markdown files for each task based on your tasks.json.` +* **Key Parameters/Options:** + * `output`: `The directory where Taskmaster should save the task files (default: in a 'tasks' directory).` (CLI: `-o, --output <directory>`) + * `tag`: `Specify which tag context to generate files for. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) +* **Usage:** Run this after making changes to tasks.json to keep individual task files up to date. This command is now manual and no longer runs automatically. + +--- + +## AI-Powered Research + +### 25. Research (`research`) + +* **MCP Tool:** `research` +* **CLI Command:** `task-master research [options]` +* **Description:** `Perform AI-powered research queries with project context to get fresh, up-to-date information beyond the AI's knowledge cutoff.` +* **Key Parameters/Options:** + * `query`: `Required. Research query/prompt (e.g., "What are the latest best practices for React Query v5?").` (CLI: `[query]` positional or `-q, --query <text>`) + * `taskIds`: `Comma-separated list of task/subtask IDs from the current tag context (e.g., "15,16.2,17").` (CLI: `-i, --id <ids>`) + * `filePaths`: `Comma-separated list of file paths for context (e.g., "src/api.js,docs/readme.md").` (CLI: `-f, --files <paths>`) + * `customContext`: `Additional custom context text to include in the research.` (CLI: `-c, --context <text>`) + * `includeProjectTree`: `Include project file tree structure in context (default: false).` (CLI: `--tree`) + * `detailLevel`: `Detail level for the research response: 'low', 'medium', 'high' (default: medium).` (CLI: `--detail <level>`) + * `saveTo`: `Task or subtask ID (e.g., "15", "15.2") to automatically save the research conversation to.` (CLI: `--save-to <id>`) + * `saveFile`: `If true, saves the research conversation to a markdown file in '.taskmaster/docs/research/'.` (CLI: `--save-file`) + * `noFollowup`: `Disables the interactive follow-up question menu in the CLI.` (CLI: `--no-followup`) + * `tag`: `Specify which tag context to use for task-based context gathering. Defaults to the current active tag.` (CLI: `--tag <name>`) + * `projectRoot`: `The directory of the project. Must be an absolute path.` (CLI: Determined automatically) +* **Usage:** **This is a POWERFUL tool that agents should use FREQUENTLY** to: + * Get fresh information beyond knowledge cutoff dates + * Research latest best practices, library updates, security patches + * Find implementation examples for specific technologies + * Validate approaches against current industry standards + * Get contextual advice based on project files and tasks +* **When to Consider Using Research:** + * **Before implementing any task** - Research current best practices + * **When encountering new technologies** - Get up-to-date implementation guidance (libraries, apis, etc) + * **For security-related tasks** - Find latest security recommendations + * **When updating dependencies** - Research breaking changes and migration guides + * **For performance optimization** - Get current performance best practices + * **When debugging complex issues** - Research known solutions and workarounds +* **Research + Action Pattern:** + * Use `research` to gather fresh information + * Use `update_subtask` to commit findings with timestamps + * Use `update_task` to incorporate research into task details + * Use `add_task` with research flag for informed task creation +* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. The research provides FRESH data beyond the AI's training cutoff, making it invaluable for current best practices and recent developments. + +--- + +## Tag Management + +This new suite of commands allows you to manage different task contexts (tags). + +### 26. List Tags (`tags`) + +* **MCP Tool:** `list_tags` +* **CLI Command:** `task-master tags [options]` +* **Description:** `List all available tags with task counts, completion status, and other metadata.` +* **Key Parameters/Options:** + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) + * `--show-metadata`: `Include detailed metadata in the output (e.g., creation date, description).` (CLI: `--show-metadata`) + +### 27. Add Tag (`add_tag`) + +* **MCP Tool:** `add_tag` +* **CLI Command:** `task-master add-tag <tagName> [options]` +* **Description:** `Create a new, empty tag context, or copy tasks from another tag.` +* **Key Parameters/Options:** + * `tagName`: `Name of the new tag to create (alphanumeric, hyphens, underscores).` (CLI: `<tagName>` positional) + * `--from-branch`: `Creates a tag with a name derived from the current git branch, ignoring the <tagName> argument.` (CLI: `--from-branch`) + * `--copy-from-current`: `Copy tasks from the currently active tag to the new tag.` (CLI: `--copy-from-current`) + * `--copy-from <tag>`: `Copy tasks from a specific source tag to the new tag.` (CLI: `--copy-from <tag>`) + * `--description <text>`: `Provide an optional description for the new tag.` (CLI: `-d, --description <text>`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) + +### 28. Delete Tag (`delete_tag`) + +* **MCP Tool:** `delete_tag` +* **CLI Command:** `task-master delete-tag <tagName> [options]` +* **Description:** `Permanently delete a tag and all of its associated tasks.` +* **Key Parameters/Options:** + * `tagName`: `Name of the tag to delete.` (CLI: `<tagName>` positional) + * `--yes`: `Skip the confirmation prompt.` (CLI: `-y, --yes`) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) + +### 29. Use Tag (`use_tag`) + +* **MCP Tool:** `use_tag` +* **CLI Command:** `task-master use-tag <tagName>` +* **Description:** `Switch your active task context to a different tag.` +* **Key Parameters/Options:** + * `tagName`: `Name of the tag to switch to.` (CLI: `<tagName>` positional) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) + +### 30. Rename Tag (`rename_tag`) + +* **MCP Tool:** `rename_tag` +* **CLI Command:** `task-master rename-tag <oldName> <newName>` +* **Description:** `Rename an existing tag.` +* **Key Parameters/Options:** + * `oldName`: `The current name of the tag.` (CLI: `<oldName>` positional) + * `newName`: `The new name for the tag.` (CLI: `<newName>` positional) + * `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`) + +### 31. Copy Tag (`copy_tag`) + +* **MCP Tool:** `copy_tag` +* **CLI Command:** `task-master copy-tag <sourceName> <targetName> [options]` +* **Description:** `Copy an entire tag context, including all its tasks and metadata, to a new tag.` +* **Key Parameters/Options:** + * `sourceName`: `Name of the tag to copy from.` (CLI: `<sourceName>` positional) + * `targetName`: `Name of the new tag to create.` (CLI: `<targetName>` positional) + * `--description <text>`: `Optional description for the new tag.` (CLI: `-d, --description <text>`) + +--- + +## Miscellaneous + +### 32. Sync Readme (`sync-readme`) -- experimental + +* **MCP Tool:** N/A +* **CLI Command:** `task-master sync-readme [options]` +* **Description:** `Exports your task list to your project's README.md file, useful for showcasing progress.` +* **Key Parameters/Options:** + * `status`: `Filter tasks by status (e.g., 'pending', 'done').` (CLI: `-s, --status <status>`) + * `withSubtasks`: `Include subtasks in the export.` (CLI: `--with-subtasks`) + * `tag`: `Specify which tag context to export from. Defaults to the current active tag.` (CLI: `--tag <name>`) + +--- + +## Environment Variables Configuration (Updated) + +Taskmaster primarily uses the **`.taskmaster/config.json`** file (in project root) for configuration (models, parameters, logging level, etc.), managed via `task-master models --setup`. + +Environment variables are used **only** for sensitive API keys related to AI providers and specific overrides like the Ollama base URL: + +* **API Keys (Required for corresponding provider):** + * `ANTHROPIC_API_KEY` + * `PERPLEXITY_API_KEY` + * `OPENAI_API_KEY` + * `GOOGLE_API_KEY` + * `MISTRAL_API_KEY` + * `AZURE_OPENAI_API_KEY` (Requires `AZURE_OPENAI_ENDPOINT` too) + * `OPENROUTER_API_KEY` + * `XAI_API_KEY` + * `OLLAMA_API_KEY` (Requires `OLLAMA_BASE_URL` too) +* **Endpoints (Optional/Provider Specific inside .taskmaster/config.json):** + * `AZURE_OPENAI_ENDPOINT` + * `OLLAMA_BASE_URL` (Default: `http://localhost:11434/api`) + +**Set API keys** in your **`.env`** file in the project root (for CLI use) or within the `env` section of your **`.cursor/mcp.json`** file (for MCP/Cursor integration). All other settings (model choice, max tokens, temperature, log level, custom endpoints) are managed in `.taskmaster/config.json` via `task-master models` command or `models` MCP tool. + +--- + +For details on how these commands fit into the development process, see the [dev_workflow.mdc](mdc:.cursor/rules/taskmaster/dev_workflow.mdc). \ No newline at end of file diff --git a/website-monitoring-frontend/.dockerignore b/website-monitoring-frontend/.dockerignore new file mode 100644 index 0000000..cab67ef --- /dev/null +++ b/website-monitoring-frontend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +npm-debug.log +.env +.git diff --git a/website-monitoring-frontend/.eslintrc.json b/website-monitoring-frontend/.eslintrc.json new file mode 100644 index 0000000..5275a92 --- /dev/null +++ b/website-monitoring-frontend/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": ["next/core-web-vitals"], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], + "react-hooks/exhaustive-deps": "warn", + "react/no-unescaped-entities": "off", + "@next/next/no-img-element": "warn" + } +} diff --git a/website-monitoring-frontend/.github/workflows/cron-scan.yml b/website-monitoring-frontend/.github/workflows/cron-scan.yml new file mode 100644 index 0000000..168efa8 --- /dev/null +++ b/website-monitoring-frontend/.github/workflows/cron-scan.yml @@ -0,0 +1,37 @@ +name: Lighthouse Scan Cron Job + +on: + schedule: + - cron: '0 */6 * * *' # Every 6 hours + workflow_dispatch: # Allow manual triggering + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - name: Trigger Scan + run: | + # Get the deployment URL from environment or use a default + DEPLOYMENT_URL="${DEPLOYMENT_URL:-https://your-domain.com}" + + echo "Triggering scan at: $DEPLOYMENT_URL/api/cron/scan?mode=all" + + # Make the API call + response=$(curl -s -w "\n%{http_code}" -X POST "$DEPLOYMENT_URL/api/cron/scan?mode=all") + + # Extract response body and status code + http_code=$(echo "$response" | tail -n1) + response_body=$(echo "$response" | head -n -1) + + echo "Response Status: $http_code" + echo "Response Body: $response_body" + + # Check if the request was successful + if [ "$http_code" -eq 200 ]; then + echo "✅ Scan triggered successfully" + else + echo "❌ Failed to trigger scan. Status: $http_code" + exit 1 + fi + env: + DEPLOYMENT_URL: ${{ secrets.DEPLOYMENT_URL }} \ No newline at end of file diff --git a/website-monitoring-frontend/.gitignore b/website-monitoring-frontend/.gitignore new file mode 100644 index 0000000..4f2aa29 --- /dev/null +++ b/website-monitoring-frontend/.gitignore @@ -0,0 +1,72 @@ +# dependencies +*/node_modules +/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 + +# Logs +logs +*.log +dev-debug.log +# Dependency directories +node_modules/ +# Environment variables +.env +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +# OS specific + +# Task files +# tasks.json +# tasks/ + +# Devenv +.devenv* +devenv.local.nix + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/website-monitoring-frontend/.taskmaster/config.json b/website-monitoring-frontend/.taskmaster/config.json new file mode 100644 index 0000000..dea0333 --- /dev/null +++ b/website-monitoring-frontend/.taskmaster/config.json @@ -0,0 +1,37 @@ +{ + "models": { + "main": { + "provider": "ollama", + "modelId": "devstral:latest", + "maxTokens": 128000, + "temperature": 0.2 + }, + "research": { + "provider": "ollama", + "modelId": "devstral:latest", + "maxTokens": 128000, + "temperature": 0.1 + }, + "fallback": { + "provider": "ollama", + "modelId": "devstral:latest", + "maxTokens": 128000, + "temperature": 0.2 + } + }, + "global": { + "logLevel": "info", + "debug": false, + "defaultNumTasks": 10, + "defaultSubtasks": 5, + "defaultPriority": "medium", + "projectName": "Taskmaster", + "ollamaBaseURL": "http://localhost:11434/api", + "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", + "responseLanguage": "English", + "defaultTag": "master", + "azureOpenaiBaseURL": "https://your-endpoint.openai.azure.com/", + "userId": "1234567890" + }, + "claudeCode": {} +} \ No newline at end of file diff --git a/website-monitoring-frontend/.taskmaster/docs/prd.txt b/website-monitoring-frontend/.taskmaster/docs/prd.txt new file mode 100644 index 0000000..b2b28e2 --- /dev/null +++ b/website-monitoring-frontend/.taskmaster/docs/prd.txt @@ -0,0 +1,59 @@ +# Website Monitoring Frontend - Error Handling & Debugging PRD + +## Project Overview +Fix console errors in dashboard pages and improve error handling throughout the website monitoring frontend application. + +## Current Issues +- Console errors showing empty objects `{}` in dashboard pages +- Poor error handling in data loading functions +- Missing environment variables for Supabase configuration +- Database connection issues causing data loading failures + +## Objectives +1. Fix console error logging to show meaningful error information +2. Implement proper error handling and user feedback +3. Set up proper environment variable configuration +4. Add error boundaries and fallback UI components +5. Improve debugging capabilities + +## Technical Requirements + +### Error Handling Improvements +- Replace `console.error("Error loading data:", error)` with proper error serialization +- Add error boundaries around dashboard components +- Implement user-friendly error messages +- Add retry mechanisms for failed data loads +- Create error logging utility functions + +### Environment Configuration +- Set up proper Supabase environment variables +- Add environment variable validation +- Create development and production configurations +- Add environment variable documentation + +### Database Connection +- Verify Supabase connection configuration +- Add connection health checks +- Implement graceful degradation when database is unavailable +- Add database schema validation + +### UI/UX Improvements +- Add loading states for all data operations +- Implement error states with actionable messages +- Add retry buttons for failed operations +- Create fallback UI for when data is unavailable + +## Success Criteria +- No more empty object console errors +- All dashboard pages show meaningful error messages +- Users can retry failed operations +- Application gracefully handles database connection issues +- Environment variables are properly configured +- Error boundaries catch and display errors appropriately + +## Implementation Priority +1. Fix console error logging (High) +2. Set up environment variables (High) +3. Add error boundaries (Medium) +4. Implement retry mechanisms (Medium) +5. Add comprehensive error handling (Low) \ No newline at end of file diff --git a/website-monitoring-frontend/.taskmaster/reports/task-complexity-report.json b/website-monitoring-frontend/.taskmaster/reports/task-complexity-report.json new file mode 100644 index 0000000..99f0954 --- /dev/null +++ b/website-monitoring-frontend/.taskmaster/reports/task-complexity-report.json @@ -0,0 +1,85 @@ +{ + "meta": { + "generatedAt": "2025-07-24T15:07:47.889Z", + "tasksAnalyzed": 9, + "totalTasks": 10, + "analysisCount": 9, + "thresholdScore": 5, + "projectName": "Taskmaster", + "usedResearch": true + }, + "complexityAnalysis": [ + { + "taskId": 2, + "taskTitle": "Implement User Authentication", + "complexityScore": 8, + "recommendedSubtasks": 5, + "expansionPrompt": "What are the key steps to implement user authentication using NextAuth.js?", + "reasoning": "User authentication is a critical feature that involves multiple flows and integrations, hence the high complexity score." + }, + { + "taskId": 3, + "taskTitle": "Create Organization & Team Management", + "complexityScore": 7, + "recommendedSubtasks": 5, + "expansionPrompt": "How can we implement CRUD operations for organizations and team members?", + "reasoning": "This task involves managing user roles and permissions, which adds to its complexity." + }, + { + "taskId": 4, + "taskTitle": "Add Website Management Features", + "complexityScore": 6, + "recommendedSubtasks": 5, + "expansionPrompt": "What are the essential features for managing monitored websites?", + "reasoning": "Website management involves multiple configurations and settings, making it moderately complex." + }, + { + "taskId": 5, + "taskTitle": "Implement Real-time and Scheduled Uptime Checks", + "complexityScore": 7, + "recommendedSubtasks": 5, + "expansionPrompt": "How can we set up real-time and scheduled uptime checks using a background job processor?", + "reasoning": "This task requires scheduling and monitoring, which adds to its complexity." + }, + { + "taskId": 6, + "taskTitle": "Develop Performance Metrics Visualization", + "complexityScore": 7, + "recommendedSubtasks": 5, + "expansionPrompt": "What are the best practices for creating performance metrics visualizations?", + "reasoning": "Visualizing data accurately and effectively is a complex task that requires careful planning." + }, + { + "taskId": 7, + "taskTitle": "Implement Alerting & Notifications System", + "complexityScore": 8, + "recommendedSubtasks": 5, + "expansionPrompt": "How can we set up configurable alerts and integrate an email service like SendGrid?", + "reasoning": "This task involves multiple integrations and configurations, making it highly complex." + }, + { + "taskId": 8, + "taskTitle": "Add Competitor Analysis Features", + "complexityScore": 7, + "recommendedSubtasks": 5, + "expansionPrompt": "What are the key features for competitor analysis and benchmarking?", + "reasoning": "Competitor analysis involves data comparison and visualization, adding to its complexity." + }, + { + "taskId": 9, + "taskTitle": "Create Main Dashboard with Key Metrics", + "complexityScore": 8, + "recommendedSubtasks": 5, + "expansionPrompt": "How can we aggregate data and create a comprehensive dashboard overview?", + "reasoning": "Creating a main dashboard involves integrating multiple data sources and visual elements." + }, + { + "taskId": 10, + "taskTitle": "Implement Responsive Design and Accessibility", + "complexityScore": 7, + "recommendedSubtasks": 5, + "expansionPrompt": "What are the best practices for implementing responsive design and accessibility?", + "reasoning": "Ensuring responsiveness and accessibility requires thorough testing and adherence to guidelines." + } + ] +} \ No newline at end of file diff --git a/website-monitoring-frontend/.taskmaster/state.json b/website-monitoring-frontend/.taskmaster/state.json new file mode 100644 index 0000000..bd38378 --- /dev/null +++ b/website-monitoring-frontend/.taskmaster/state.json @@ -0,0 +1,6 @@ +{ + "currentTag": "master", + "lastSwitched": "2025-07-24T12:45:40.600Z", + "branchTagMapping": {}, + "migrationNoticeShown": true +} \ No newline at end of file diff --git a/website-monitoring-frontend/.taskmaster/tasks/tasks.json b/website-monitoring-frontend/.taskmaster/tasks/tasks.json new file mode 100644 index 0000000..82d34cd --- /dev/null +++ b/website-monitoring-frontend/.taskmaster/tasks/tasks.json @@ -0,0 +1,980 @@ +{ + "master": { + "tasks": [ + { + "id": 1, + "title": "Setup Project Repository", + "description": "Initialize the project repository with Next.js, React, TypeScript, and Tailwind CSS.", + "details": "Create a new Next.js project using `create-next-app` with TypeScript template. Install Tailwind CSS following official documentation.", + "testStrategy": "Verify that the project structure is correct and Tailwind CSS styles are applied.", + "priority": "high", + "dependencies": [], + "status": "done", + "subtasks": [] + }, + { + "id": 2, + "title": "Implement User Authentication", + "description": "Setup user authentication with sign up, login, password reset, and OAuth/social login support.", + "details": "Use NextAuth.js for authentication. Implement sign-up, login, and password reset flows. Integrate OAuth providers like Google, Facebook.", + "testStrategy": "Test all authentication flows to ensure they work correctly.", + "priority": "high", + "dependencies": [ + 1 + ], + "status": "in-progress", + "subtasks": [ + { + "id": 1, + "title": "Setup NextAuth.js Configuration", + "description": "Configure NextAuth.js for the project.", + "dependencies": [], + "details": "Install NextAuth.js and configure it in your Next.js application. Set up basic authentication providers like credentials, Google, and Facebook.", + "status": "done", + "testStrategy": "Verify that NextAuth.js is correctly configured by attempting to log in with different providers." + }, + { + "id": 2, + "title": "Implement Sign-Up Flow", + "description": "Create a sign-up page and handle user registration.", + "dependencies": [ + "2.1" + ], + "details": "Develop a sign-up form that collects user information and uses NextAuth.js to create a new user account.", + "status": "done", + "testStrategy": "Test the sign-up flow by creating new accounts and verifying they are stored correctly." + }, + { + "id": 3, + "title": "Implement Login Flow", + "description": "Create a login page and handle user authentication.", + "dependencies": [ + "2.1" + ], + "details": "Develop a login form that uses NextAuth.js to authenticate users with their credentials or OAuth providers.", + "status": "in-progress", + "testStrategy": "Test the login flow by logging in with different accounts and ensuring proper redirection." + }, + { + "id": 4, + "title": "Implement Password Reset Flow", + "description": "Create a password reset mechanism for users.", + "dependencies": [ + "2.1" + ], + "details": "Develop a password reset form that allows users to request a password reset and update their password using NextAuth.js.", + "status": "pending", + "testStrategy": "Test the password reset flow by resetting passwords and verifying that users can log in with the new credentials." + }, + { + "id": 5, + "title": "Integrate OAuth Providers", + "description": "Configure and test OAuth providers like Google and Facebook.", + "dependencies": [ + "2.1" + ], + "details": "Set up OAuth provider configurations in NextAuth.js for Google and Facebook. Ensure that users can log in using these providers.", + "status": "pending", + "testStrategy": "Test the OAuth login flow by logging in with Google and Facebook accounts and verifying proper authentication." + } + ] + }, + { + "id": 3, + "title": "Create Organization & Team Management", + "description": "Allow users to create and join organizations, manage team members, and set organization-level dashboards.", + "details": "Implement CRUD operations for organizations and team members. Create organization-specific settings and dashboards.", + "testStrategy": "Verify that users can create organizations, invite team members, and access organization-specific dashboards.", + "priority": "medium", + "dependencies": [ + 2 + ], + "status": "pending", + "subtasks": [ + { + "id": 1, + "title": "Implement Organization CRUD Operations", + "description": "Create endpoints and UI components to allow users to create, read, update, and delete organizations.", + "dependencies": [], + "details": "Use RESTful API principles for backend implementation. Create React components for frontend forms and tables.", + "status": "pending", + "testStrategy": "Verify each CRUD operation works correctly through the UI and API." + }, + { + "id": 2, + "title": "Implement Team Member Management", + "description": "Create functionality to add, remove, and manage team members within organizations.", + "dependencies": [ + "3.1" + ], + "details": "Extend organization model to include team members. Create UI components for managing team members.", + "status": "pending", + "testStrategy": "Test adding, removing, and updating team members through the UI." + }, + { + "id": 3, + "title": "Create Organization Settings", + "description": "Develop settings specific to organizations that can be configured by administrators.", + "dependencies": [ + "3.1" + ], + "details": "Add settings model to organization schema. Create admin-only UI for configuring these settings.", + "status": "pending", + "testStrategy": "Verify settings are saved correctly and reflected in the application." + }, + { + "id": 4, + "title": "Implement Organization Dashboards", + "description": "Create dashboards that display organization-specific data and metrics.", + "dependencies": [ + "3.1", + "3.2", + "3.3" + ], + "details": "Design dashboard UI components. Fetch and display relevant data from the backend API.", + "status": "pending", + "testStrategy": "Ensure dashboards show accurate, up-to-date information specific to each organization." + }, + { + "id": 5, + "title": "Integrate Organization Management with User Authentication", + "description": "Ensure that only authenticated users can create and manage organizations and team members.", + "dependencies": [], + "details": "Use authentication middleware to protect organization management routes. Update UI components to handle authentication states.", + "status": "pending", + "testStrategy": "Test that unauthorized access attempts are properly blocked." + } + ] + }, + { + "id": 4, + "title": "Add Website Management Features", + "description": "Enable users to add, edit, and remove monitored websites with configurable settings.", + "details": "Create forms for adding/editing websites. Implement configuration options like scan frequency and alert thresholds.", + "testStrategy": "Test the website management interface to ensure all CRUD operations work correctly.", + "priority": "medium", + "dependencies": [ + 3 + ], + "status": "pending", + "subtasks": [ + { + "id": 1, + "title": "Create Website Form Component", + "description": "Develop the form component for adding new websites.", + "dependencies": [], + "details": "Use React and TypeScript to create a form with fields for website URL, name, and other basic information. Implement validation for required fields.", + "status": "pending", + "testStrategy": "Test the form rendering and validation rules." + }, + { + "id": 2, + "title": "Implement Website Addition API", + "description": "Create an API endpoint to handle adding new websites.", + "dependencies": [ + "4.1" + ], + "details": "Use Next.js API routes to create an endpoint that accepts website data and stores it in the database. Ensure proper error handling and validation.", + "status": "pending", + "testStrategy": "Test the API endpoint with various inputs, including edge cases." + }, + { + "id": 3, + "title": "Create Configuration Form Component", + "description": "Develop the form component for configuring website settings like scan frequency and alert thresholds.", + "dependencies": [], + "details": "Use React and TypeScript to create a form with fields for scan frequency, alert thresholds, and other configurable settings. Implement validation for required fields.", + "status": "pending", + "testStrategy": "Test the form rendering and validation rules." + }, + { + "id": 4, + "title": "Implement Configuration Update API", + "description": "Create an API endpoint to handle updating website configurations.", + "dependencies": [ + "4.3" + ], + "details": "Use Next.js API routes to create an endpoint that accepts configuration data and updates it in the database. Ensure proper error handling and validation.", + "status": "pending", + "testStrategy": "Test the API endpoint with various inputs, including edge cases." + }, + { + "id": 5, + "title": "Implement Website Removal Functionality", + "description": "Create an API endpoint to handle removing websites.", + "dependencies": [ + "4.1", + "4.3" + ], + "details": "Use Next.js API routes to create an endpoint that accepts a website ID and removes it from the database. Ensure proper error handling and validation.", + "status": "pending", + "testStrategy": "Test the API endpoint with various inputs, including edge cases." + } + ] + }, + { + "id": 5, + "title": "Implement Real-time and Scheduled Uptime Checks", + "description": "Setup real-time and scheduled uptime checks for monitored websites.", + "details": "Use a background job processor like Agenda or Bull to schedule uptime checks. Implement health check endpoints.", + "testStrategy": "Verify that uptime checks are performed as scheduled and results are recorded.", + "priority": "medium", + "dependencies": [ + 4 + ], + "status": "pending", + "subtasks": [ + { + "id": 1, + "title": "Setup Background Job Processor", + "description": "Choose and configure a background job processor for scheduling uptime checks.", + "dependencies": [], + "details": "Select either Agenda or Bull as the background job processor. Install the package using npm/yarn and set up basic configuration in your project.", + "status": "pending", + "testStrategy": "Verify that the job processor is correctly installed and configured by running a simple test job." + }, + { + "id": 2, + "title": "Create Health Check Endpoint", + "description": "Implement an endpoint to perform health checks on monitored websites.", + "dependencies": [], + "details": "Create an API endpoint that accepts a URL parameter and returns the status of the website (up/down). Use HTTP requests to check the website's availability.", + "status": "pending", + "testStrategy": "Test the endpoint with various URLs to ensure it correctly reports the status of each website." + }, + { + "id": 3, + "title": "Schedule Regular Uptime Checks", + "description": "Configure regular uptime checks using the background job processor.", + "dependencies": [ + "5.1", + "5.2" + ], + "details": "Set up recurring jobs in the background job processor to call the health check endpoint at specified intervals (e.g., every 5 minutes). Store the results of each check.", + "status": "pending", + "testStrategy": "Verify that uptime checks are performed as scheduled and results are recorded correctly." + }, + { + "id": 4, + "title": "Implement Real-time Uptime Checks", + "description": "Add functionality for real-time uptime checks on demand.", + "dependencies": [ + "5.2" + ], + "details": "Create an API endpoint that triggers an immediate health check and returns the result. This should bypass the scheduled job processor.", + "status": "pending", + "testStrategy": "Test the real-time endpoint with various URLs to ensure it correctly reports the status of each website immediately." + }, + { + "id": 5, + "title": "Store and Display Uptime Check Results", + "description": "Implement storage and display for uptime check results.", + "dependencies": [ + "5.3" + ], + "details": "Create a database schema to store the results of each uptime check (timestamp, URL, status). Implement a UI component to display historical uptime data.", + "status": "pending", + "testStrategy": "Verify that uptime check results are stored correctly in the database and displayed accurately in the UI." + } + ] + }, + { + "id": 6, + "title": "Develop Performance Metrics Visualization", + "description": "Create visualizations for real-time and historical performance metrics.", + "details": "Use libraries like Recharts or Chart.js to create charts and graphs. Fetch data from the backend API.", + "testStrategy": "Ensure that performance metrics are displayed correctly in the dashboard.", + "priority": "medium", + "dependencies": [ + 5 + ], + "status": "pending", + "subtasks": [ + { + "id": 1, + "title": "Setup Data Fetching from Backend API", + "description": "Create functions to fetch real-time and historical performance metrics data from the backend API.", + "dependencies": [], + "details": "Use Axios or Fetch API to create functions that will retrieve data from predefined endpoints in the backend. Handle authentication if required.", + "status": "pending", + "testStrategy": "Verify that data is fetched correctly by checking response structure and content." + }, + { + "id": 2, + "title": "Select Charting Library", + "description": "Choose between Recharts or Chart.js for creating visualizations based on project requirements.", + "dependencies": [], + "details": "Evaluate both libraries in terms of features, ease of use, and integration with the existing tech stack. Make a decision and document it.", + "status": "pending", + "testStrategy": "Create simple test charts to ensure the chosen library works well within the project setup." + }, + { + "id": 3, + "title": "Design Chart Components", + "description": "Create reusable chart components for different types of performance metrics visualizations.", + "dependencies": [ + "6.2" + ], + "details": "Develop React components using the selected charting library that can be reused across different parts of the application. Include props for customization.", + "status": "pending", + "testStrategy": "Test each component with sample data to ensure they render correctly and are responsive." + }, + { + "id": 4, + "title": "Integrate Data Fetching with Chart Components", + "description": "Connect the chart components with the data fetching functions to display real-time and historical metrics.", + "dependencies": [ + "6.1", + "6.3" + ], + "details": "Use React hooks like useEffect and useState to fetch data when components mount and update the charts accordingly.", + "status": "pending", + "testStrategy": "Verify that charts update correctly when new data is fetched and handle loading states appropriately." + }, + { + "id": 5, + "title": "Implement Real-time Data Updates", + "description": "Setup WebSocket or long-polling to receive real-time updates for performance metrics.", + "dependencies": [ + "6.1", + "6.4" + ], + "details": "Integrate a real-time data connection using WebSockets or server-sent events (SSE) to keep charts updated with the latest data.", + "status": "pending", + "testStrategy": "Simulate real-time data changes and verify that charts update instantly without requiring manual refresh." + } + ] + }, + { + "id": 7, + "title": "Implement Alerting & Notifications System", + "description": "Setup configurable alerts for downtime, slow performance, or errors with email notifications.", + "details": "Create alert configurations and integrate an email service like SendGrid. Implement in-app alert indicators.", + "testStrategy": "Test alert triggers and ensure emails are sent as expected.", + "priority": "medium", + "dependencies": [ + 6 + ], + "status": "pending", + "subtasks": [ + { + "id": 1, + "title": "Design Alert Configuration Schema", + "description": "Create a schema for alert configurations including conditions and thresholds.", + "dependencies": [], + "details": "Define JSON schema for alert configurations that includes fields like condition type (downtime, slow performance, errors), threshold values, and notification settings. Store these configurations in the database.", + "status": "pending", + "testStrategy": "Validate schema against sample configurations" + }, + { + "id": 2, + "title": "Integrate Email Service", + "description": "Set up SendGrid or similar email service for sending notifications.", + "dependencies": [], + "details": "Create an account on SendGrid, configure API keys, and set up a service in the application to send emails using these credentials. Implement functions to send test emails.", + "status": "pending", + "testStrategy": "Send test emails through the integrated service" + }, + { + "id": 3, + "title": "Implement Alert Trigger Logic", + "description": "Create logic to trigger alerts based on configured conditions.", + "dependencies": [ + "7.1" + ], + "details": "Write functions that check system metrics against alert configurations and trigger notifications when conditions are met. This can be done using background jobs or scheduled tasks.", + "status": "pending", + "testStrategy": "Simulate conditions to verify alert triggers" + }, + { + "id": 4, + "title": "Create In-App Alert Indicators", + "description": "Implement visual indicators in the application for active alerts.", + "dependencies": [ + "7.3" + ], + "details": "Design and implement UI components that display alert statuses (e.g., icons, badges) on relevant dashboards or pages within the application.", + "status": "pending", + "testStrategy": "Verify that alert indicators appear correctly when conditions are met" + }, + { + "id": 5, + "title": "Test End-to-End Alerting System", + "description": "Perform end-to-end testing of the entire alerting and notification system.", + "dependencies": [ + "7.2", + "7.4" + ], + "details": "Create test cases that simulate various alert conditions, verify that alerts are triggered correctly, and ensure that notifications are sent via email and displayed in-app.", + "status": "pending", + "testStrategy": "Run automated tests to validate the entire workflow" + } + ] + }, + { + "id": 8, + "title": "Add Competitor Analysis Features", + "description": "Enable users to add competitor websites for benchmarking and compare performance metrics.", + "details": "Create forms for adding competitors. Implement comparison visualizations in the dashboard.", + "testStrategy": "Verify that competitor analysis features work correctly and comparisons are accurate.", + "priority": "medium", + "dependencies": [ + 7 + ], + "status": "pending", + "subtasks": [ + { + "id": 1, + "title": "Create Competitor Addition Form", + "description": "Develop a form interface for users to add competitor websites.", + "dependencies": [], + "details": "Use React and Tailwind CSS to create the form. Include fields for website URL, name, and notes. Implement validation rules for required fields.", + "status": "pending", + "testStrategy": "Verify that the form renders correctly and validates input properly." + }, + { + "id": 2, + "title": "Implement Competitor Data Storage", + "description": "Set up backend storage for competitor website data.", + "dependencies": [ + "8.1" + ], + "details": "Create a database schema to store competitor information. Implement API endpoints to save and retrieve competitor data.", + "status": "pending", + "testStrategy": "Test the API endpoints to ensure they correctly handle CRUD operations for competitor data." + }, + { + "id": 3, + "title": "Fetch Competitor Performance Metrics", + "description": "Retrieve performance metrics for added competitors from backend services.", + "dependencies": [ + "8.2" + ], + "details": "Implement API calls to fetch performance metrics for each competitor website. Store the retrieved data in a structured format.", + "status": "pending", + "testStrategy": "Verify that the correct performance metrics are fetched and stored for each competitor." + }, + { + "id": 4, + "title": "Develop Comparison Visualization", + "description": "Create visualizations to compare performance metrics between user's website and competitors.", + "dependencies": [ + "8.3" + ], + "details": "Use a charting library like Recharts or Chart.js to create comparison charts. Display key metrics such as uptime, response time, etc.", + "status": "pending", + "testStrategy": "Ensure that the visualizations accurately reflect the performance comparisons between websites." + }, + { + "id": 5, + "title": "Integrate Visualization into Dashboard", + "description": "Add the comparison visualization to the main dashboard interface.", + "dependencies": [ + "8.4" + ], + "details": "Embed the created visualizations within the existing dashboard layout. Ensure they are responsive and fit well with other UI elements.", + "status": "pending", + "testStrategy": "Verify that the visualizations display correctly in the dashboard and provide meaningful insights." + } + ] + }, + { + "id": 9, + "title": "Create Main Dashboard with Key Metrics", + "description": "Develop the main dashboard to display key metrics and status for all monitored websites.", + "details": "Aggregate data from various sources and create a comprehensive overview. Use modern UI components.", + "testStrategy": "Ensure that the main dashboard displays accurate and up-to-date information.", + "priority": "medium", + "dependencies": [ + 8 + ], + "status": "pending", + "subtasks": [ + { + "id": 1, + "title": "Design Dashboard Layout", + "description": "Create the layout structure for the main dashboard using modern UI components.", + "dependencies": [], + "details": "Use a responsive grid system to create sections for different metrics. Ensure the layout is flexible and can accommodate various widgets.", + "status": "pending", + "testStrategy": "Verify that the layout adapts correctly to different screen sizes." + }, + { + "id": 2, + "title": "Integrate Data Aggregation API", + "description": "Connect the dashboard to the data aggregation service to fetch key metrics.", + "dependencies": [ + "9.1" + ], + "details": "Implement API calls to retrieve data from various sources and structure it for display on the dashboard.", + "status": "pending", + "testStrategy": "Test that data is fetched correctly and displayed accurately." + }, + { + "id": 3, + "title": "Create Metric Widgets", + "description": "Develop reusable UI components to display individual metrics.", + "dependencies": [ + "9.1" + ], + "details": "Design widgets for different types of data (e.g., charts, gauges, tables) and ensure they are configurable.", + "status": "pending", + "testStrategy": "Verify that each widget displays data correctly and can be configured as needed." + }, + { + "id": 4, + "title": "Implement Status Indicators", + "description": "Add visual indicators to show the status of monitored websites (e.g., online/offline, error alerts).", + "dependencies": [ + "9.2", + "9.3" + ], + "details": "Use color coding and icons to represent different statuses clearly.", + "status": "pending", + "testStrategy": "Test that status indicators update correctly based on website conditions." + }, + { + "id": 5, + "title": "Add Real-time Updates", + "description": "Enable real-time data updates for the dashboard using WebSockets or similar technology.", + "dependencies": [ + "9.2", + "9.3", + "9.4" + ], + "details": "Implement a mechanism to push updates from the server to the client and refresh the dashboard accordingly.", + "status": "pending", + "testStrategy": "Verify that changes in data are reflected on the dashboard in real-time." + } + ] + }, + { + "id": 10, + "title": "Implement Responsive Design and Accessibility", + "description": "Ensure the application is responsive and accessible on various devices.", + "details": "Use Tailwind CSS to create a responsive layout. Follow accessibility best practices and test with tools like Lighthouse.", + "testStrategy": "Verify that the application works well on different screen sizes and passes accessibility checks.", + "priority": "medium", + "dependencies": [ + 9 + ], + "status": "pending", + "subtasks": [ + { + "id": 1, + "title": "Integrate Tailwind CSS for Responsive Layout", + "description": "Set up Tailwind CSS in the project to create responsive layouts.", + "dependencies": [], + "details": "Install Tailwind CSS via npm and configure it in your project's CSS setup. Create base styles using Tailwind's utility classes.", + "status": "pending", + "testStrategy": "Verify that basic Tailwind CSS classes are working and responsive on different screen sizes." + }, + { + "id": 2, + "title": "Create Responsive Layout Components", + "description": "Develop key layout components using Tailwind CSS's responsive utilities.", + "dependencies": [ + "10.1" + ], + "details": "Implement header, footer, and main content sections with responsive behavior. Use Tailwind's grid system and breakpoints.", + "status": "pending", + "testStrategy": "Test the layout on various screen sizes to ensure it adapts correctly." + }, + { + "id": 3, + "title": "Implement Accessibility Best Practices", + "description": "Apply accessibility best practices throughout the application.", + "dependencies": [ + "10.2" + ], + "details": "Ensure proper use of semantic HTML, ARIA roles, and keyboard navigation support. Implement color contrast checks.", + "status": "pending", + "testStrategy": "Use tools like Lighthouse to audit accessibility and fix any issues identified." + }, + { + "id": 4, + "title": "Test Accessibility with Lighthouse", + "description": "Run accessibility tests using Lighthouse to identify and fix issues.", + "dependencies": [ + "10.3" + ], + "details": "Configure Lighthouse in your development environment and run regular audits. Address any critical accessibility issues found.", + "status": "pending", + "testStrategy": "Verify that the application passes Lighthouse accessibility checks with a high score." + }, + { + "id": 5, + "title": "Final Responsive Design Review", + "description": "Conduct a comprehensive review of the responsive design and accessibility implementation.", + "dependencies": [ + "10.4" + ], + "details": "Perform manual testing on various devices and screen sizes. Ensure all accessibility best practices are followed consistently.", + "status": "pending", + "testStrategy": "Verify that the application is fully responsive and accessible across different devices and browsers." + } + ] + }, + { + "id": 11, + "title": "Fix Console Error Logging", + "description": "Replace console.error with proper error serialization to show meaningful information.", + "status": "done", + "dependencies": [], + "priority": "high", + "details": "Use utility functions from `src/utils/errorUtils.ts` for enhanced error logging:\n- `serializeError()`: Safely serializes error objects\n- `logError()`: Enhanced console.error with context\n- `getUserFriendlyErrorMessage()`: Converts technical errors to user-friendly messages\n- Error type detection functions: `isDatabaseError()`, `isNetworkError()`, `isAuthError()`\n\nUpdated all dashboard pages (Performance, SEO, Monitoring, Alerts) to use the new error logging with enhanced context including organization ID, function name, time range, timestamp, and user agent information.", + "testStrategy": "Verify that console logs contain detailed error information instead of empty objects. Check that errors are logged consistently across all dashboard pages with proper context.", + "subtasks": [ + { + "id": 1, + "title": "Create comprehensive error utility functions in `src/utils/errorUtils.ts`", + "description": "Implement the following utility functions:\n- `serializeError()`\n- `logError()`\n- `getUserFriendlyErrorMessage()`\n- Error type detection functions: `isDatabaseError()`, `isNetworkError()`, `isAuthError()`", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 2, + "title": "Update Performance page error logging", + "description": "Replace `console.error('Error loading performance data:', error)` with `logError()` including context.", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 3, + "title": "Update SEO page error handling", + "description": "Update error handling to use proper serialization and enhanced context.", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 4, + "title": "Enhance Monitoring page error logging", + "description": "Improve error logging with organization context.", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 5, + "title": "Improve Alerts page error handling", + "description": "Update error handling to include detailed context.", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + } + ] + }, + { + "id": 12, + "title": "Set Up Environment Variables for Supabase", + "description": "Configure environment variables required for Supabase connection.", + "status": "done", + "dependencies": [], + "priority": "high", + "details": "Create `.env` files for development and production with necessary Supabase configurations. Use `dotenv` package to load them.\n\n**What was implemented:**\n1. **Environment Variables Configuration**:\n - ✅ Supabase environment variables are already properly configured in `.env`\n - ✅ Created comprehensive `.env.example` file with all required variables\n - ✅ Enhanced `src/lib/supabase.ts` with better validation and error handling\n2. **Enhanced Supabase Configuration**:\n - ✅ Added environment variable validation with specific error messages\n - ✅ Added URL format validation for Supabase URL\n - ✅ Created `testSupabaseConnection()` function for connection testing\n - ✅ Added `validateEnvironment()` function to check required variables\n3. **Environment Validation Component**:\n - ✅ Created `EnvironmentValidator` component for UI-based environment checking\n - ✅ Includes connection testing functionality\n - ✅ Provides setup instructions for missing variables\n - ✅ Shows detailed status of environment configuration\n4. **Key Environment Variables Configured**:\n - ✅ `NEXT_PUBLIC_SUPABASE_URL`: https://revhjskovnhnmmuorjzs.supabase.co\n - ✅ `NEXT_PUBLIC_SUPABASE_ANON_KEY`: Properly configured\n - ✅ `SUPABASE_SERVICE_ROLE_KEY`: Available for server-side operations", + "testStrategy": "Verify that the application can connect to Supabase using environment variables.\n\n**Verification**:\n- ✅ Build process completes successfully with environment variables loaded\n- ✅ Supabase client initializes without errors\n- ✅ Environment validation functions work correctly\n- ✅ Connection testing functionality implemented", + "subtasks": [ + { + "id": 1, + "title": "Configure Supabase environment variables in `.env`", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 2, + "title": "Create comprehensive `.env.example` file with all required variables", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 3, + "title": "Enhance `src/lib/supabase.ts` with better validation and error handling", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 4, + "title": "Add environment variable validation with specific error messages", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 5, + "title": "Add URL format validation for Supabase URL", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 6, + "title": "Create `testSupabaseConnection()` function for connection testing", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 7, + "title": "Add `validateEnvironment()` function to check required variables", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 8, + "title": "Create `EnvironmentValidator` component for UI-based environment checking", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 9, + "title": "Include connection testing functionality in EnvironmentValidator", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 10, + "title": "Provide setup instructions for missing variables in EnvironmentValidator", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 11, + "title": "Show detailed status of environment configuration in EnvironmentValidator", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + } + ] + }, + { + "id": 13, + "title": "Add Error Boundaries Around Dashboard Components", + "description": "Implement error boundaries to catch and display errors in dashboard components.", + "details": "Use React's `ErrorBoundary` component or a custom implementation. Wrap dashboard components with the error boundary.", + "testStrategy": "Simulate errors in dashboard components and verify that they are caught by the error boundaries.", + "priority": "medium", + "dependencies": [ + 12 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 14, + "title": "Implement Retry Mechanisms for Failed Data Loads", + "description": "Add retry functionality for data loading operations that fail.", + "details": "Use a library like `retry` or implement custom retry logic with exponential backoff. Integrate with data fetching functions.", + "testStrategy": "Simulate network failures and verify that the application retries data loading operations.", + "priority": "medium", + "dependencies": [ + 12 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 15, + "title": "Create Error Logging Utility Functions", + "description": "Develop utility functions for consistent error logging across the application.", + "details": "Create a centralized error logging function that handles serialization and sends logs to a monitoring service if needed.", + "testStrategy": "Verify that errors are logged consistently using the utility functions.", + "priority": "low", + "dependencies": [ + 12 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 16, + "title": "Implement User-Friendly Error Messages", + "description": "Display user-friendly error messages when data loading fails or other errors occur.", + "details": "Update UI components to show meaningful error messages. Use a library like `react-toastify` for notifications.", + "testStrategy": "Simulate errors and verify that user-friendly error messages are displayed.", + "priority": "medium", + "dependencies": [ + 13, + 15 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 17, + "title": "Add Loading States for Data Operations", + "description": "Implement loading states to indicate data operations in progress.", + "details": "Use a library like `react-loader-spinner` or create custom loading indicators. Integrate with data fetching functions.", + "testStrategy": "Verify that loading states are displayed during data operations.", + "priority": "medium", + "dependencies": [ + 12 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 18, + "title": "Implement Graceful Degradation for Database Unavailability", + "description": "Handle database connection issues gracefully and provide fallback UI.", + "details": "Add health checks for the Supabase connection. Implement fallback UI components to display when data is unavailable.", + "testStrategy": "Simulate database unavailability and verify that the application handles it gracefully with fallback UI.", + "priority": "medium", + "dependencies": [ + 12, + 13 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 19, + "title": "Fix Browser Error Serialization", + "description": "Ensure errors display detailed information instead of 'Object' in browser environment.", + "status": "done", + "dependencies": [ + 1, + 13, + 15 + ], + "priority": "high", + "details": "Investigate why errors are showing as 'Object' in the browser console. Implement proper serialization for error objects to display meaningful information. This may involve updating error handling code and ensuring all relevant error properties are included in the serialized output.\n\n**What was implemented:**\n1. **Enhanced Error Serialization** (`src/utils/errorUtils.ts`):\n - ✅ Improved `serializeError()` function to better handle browser-side errors\n - ✅ Added specific handling for Supabase PostgrestError objects\n - ✅ Enhanced error extraction for objects that might be error-like\n - ✅ Added fallback serialization with better error type detection\n2. **Improved Error Logging**:\n - ✅ Enhanced `logError()` function with better error extraction\n - ✅ Added error type detection and additional debugging info\n - ✅ Improved handling of non-serializable objects\n - ✅ Added development-mode debugging with error keys and prototype info\n3. **Supabase Error Handling**:\n - ✅ Created `extractSupabaseErrorInfo()` function for detailed Supabase error extraction\n - ✅ Added specific handling for PostgrestError, details, hints, and codes\n - ✅ Enhanced error context with Supabase-specific information\n4. **Updated All Dashboard Pages**:\n - ✅ **Performance page**: Enhanced error handling with Supabase error extraction\n - ✅ **SEO page**: Improved error logging with detailed context\n - ✅ **Monitoring page**: Added Supabase error info to error logs\n - ✅ **Alerts page**: Enhanced error context with detailed error information", + "testStrategy": "Simulate various error scenarios in the application and verify that detailed error information is displayed in the browser console instead of 'Object'. Test across different browsers to ensure consistency.", + "subtasks": [] + }, + { + "id": 20, + "title": "Set Up Missing Database Tables", + "description": "Create required database tables (scans, scan_results, pages, alerts) in Supabase to support dashboard functionality and resolve 400 errors.", + "status": "done", + "dependencies": [ + 9, + 18 + ], + "priority": "high", + "details": "1. Identify missing tables based on application requirements.\n2. Define schema for each table including columns and data types.\n3. Use Supabase SQL editor or API to create the tables.\n4. Verify table creation and structure using Supabase tools.\n5. Update any necessary database migration scripts.\n6. Create comprehensive database setup script (setup-database.sql) with complete schema, relationships, constraints, sample data, RLS policies, and performance indexes.\n7. Enhance DatabaseSetupHelper component to check for specific missing tables causing 400 errors, add 'Copy SQL' functionality, provide clear instructions, and visual indicators.", + "testStrategy": "1. Check that all required tables exist in Supabase.\n2. Verify table schemas match requirements.\n3. Test dashboard functionality to ensure it can read/write data from/to the new tables.\n4. Run any existing database tests to confirm integration.\n5. Verify that 400 errors for scans, websites, and alerts are resolved after running the database setup script.\n6. Confirm that dashboard pages load successfully with sample data available for testing.", + "subtasks": [ + { + "id": 1, + "title": "Identify missing tables based on application requirements", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 2, + "title": "Define schema for each table including columns and data types", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 3, + "title": "Use Supabase SQL editor or API to create the tables", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 4, + "title": "Verify table creation and structure using Supabase tools", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 5, + "title": "Update any necessary database migration scripts", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 6, + "title": "Create comprehensive database setup script (setup-database.sql)", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 7, + "title": "Enhance DatabaseSetupHelper component", + "description": "", + "status": "completed", + "dependencies": [], + "details": "", + "testStrategy": "" + } + ] + }, + { + "id": 21, + "title": "Build Automatic Lighthouse Scanner System", + "description": "Develop a system for automatic Lighthouse scanning with change detection and periodic scanning based on subscription tiers.", + "status": "done", + "dependencies": [ + 18, + 20 + ], + "priority": "high", + "details": "✅ **Task Completed Successfully**\n\n**Automatic Lighthouse Scanner System Implemented**\n\n**Core Components Created:**\n\n1. **LighthouseScanner Service** (`src/services/lighthouseScanner.ts`):\n - ✅ Change detection using content hash comparison\n - ✅ Subscription-based rate limiting and limits checking\n - ✅ Comprehensive scan execution with metrics collection\n - ✅ Support for multiple device types and scan categories\n - ✅ Automatic scan result storage and metric tracking\n\n2. **ScanScheduler Service** (`src/services/scanScheduler.ts`):\n - ✅ Periodic scan scheduling based on frequency settings\n - ✅ Change detection processing for all websites\n - ✅ Subscription tier validation and feature availability\n - ✅ Next run time calculation and schedule management\n\n3. **Enhanced Cron Handler** (`src/app/api/cron/scan/route.ts`):\n - ✅ Orchestrates both scheduled and change detection scans\n - ✅ Manual scan triggering with subscription validation\n - ✅ Comprehensive scan statistics and monitoring\n - ✅ Error handling and logging for all scan operations\n\n4. **Webhook Handler** (`src/app/api/webhooks/website-change/route.ts`):\n - ✅ External change detection webhook endpoint\n - ✅ Automatic scan triggering on website changes\n - ✅ Subscription validation and rate limiting\n - ✅ Audit logging for change detection events\n\n5. **ScanScheduleManager Component** (`src/components/dashboard/ScanScheduleManager.tsx`):\n - ✅ User interface for managing scan schedules\n - ✅ Subscription tier display and feature availability\n - ✅ Usage tracking and limit monitoring\n - ✅ Manual scan triggering with validation\n\n**Key Features Implemented:**\n\n**Subscription-Based Features:**\n- **Free Tier**: 5 scans/day, 50/month, no change detection, no scheduled scans\n- **Starter Tier**: 20 scans/day, 200/month, change detection enabled, daily scheduled scans\n- **Professional Tier**: 100 scans/day, 1000/month, change detection enabled, hourly scheduled scans\n- **Enterprise Tier**: 500 scans/day, 5000/month, change detection enabled, hourly scheduled scans\n\n**Change Detection:**\n- Content hash comparison to detect website changes\n- Automatic high-priority scans when changes are detected\n- Webhook support for external change notifications\n- Audit logging for all change detection events\n\n**Scheduled Scanning:**\n- Configurable frequencies: hourly, daily, weekly, monthly\n- Device type selection: desktop, mobile, or both\n- Category selection: performance, accessibility, SEO, best practices\n- Automatic schedule management and next run time calculation\n\n**Rate Limiting & Monitoring:**\n- Daily and monthly scan limits per subscription tier\n- Real-time usage tracking and limit enforcement\n- Comprehensive scan statistics and reporting\n- Error handling and logging for all operations\n\n**Integration Points:**\n- Seamless integration with existing dashboard components\n- Database schema compatibility with existing tables\n- API endpoints for external integrations\n- Webhook support for third-party change detection\n\n**Usage Instructions:**\n1. **Setup Cron Jobs**: Configure cron to call `/api/cron/scan` at desired intervals\n2. **Configure Webhooks**: Set up webhook endpoints for external change detection\n3. **Manage Schedules**: Use the ScanScheduleManager component in the dashboard\n4. **Monitor Usage**: Track scan usage and limits through the dashboard interface\n\nThe system is now ready for production use with full subscription-based feature control and automatic scanning capabilities.", + "testStrategy": "1. Verify that change detection triggers scans accurately when website content changes.\n2. Test periodic scanning schedules for different subscription tiers.\n3. Check that scan results are stored correctly in the Supabase database.\n4. Ensure error handling works as expected with meaningful error messages.\n5. Validate UI components display scan results and statuses correctly.\n6. Confirm notifications are sent for scan completions and critical issues.", + "subtasks": [] + } + ], + "metadata": { + "created": "2025-07-24T14:16:43.871Z", + "updated": "2025-08-04T10:51:41.627Z", + "description": "Tasks for master context" + } + } +} \ No newline at end of file diff --git a/website-monitoring-frontend/.taskmaster/templates/example_prd.txt b/website-monitoring-frontend/.taskmaster/templates/example_prd.txt new file mode 100644 index 0000000..194114d --- /dev/null +++ b/website-monitoring-frontend/.taskmaster/templates/example_prd.txt @@ -0,0 +1,47 @@ +<context> +# Overview +[Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.] + +# Core Features +[List and describe the main features of your product. For each feature, include: +- What it does +- Why it's important +- How it works at a high level] + +# User Experience +[Describe the user journey and experience. Include: +- User personas +- Key user flows +- UI/UX considerations] +</context> +<PRD> +# Technical Architecture +[Outline the technical implementation details: +- System components +- Data models +- APIs and integrations +- Infrastructure requirements] + +# Development Roadmap +[Break down the development process into phases: +- MVP requirements +- Future enhancements +- Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks] + +# Logical Dependency Chain +[Define the logical order of development: +- Which features need to be built first (foundation) +- Getting as quickly as possible to something usable/visible front end that works +- Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches] + +# Risks and Mitigations +[Identify potential risks and how they'll be addressed: +- Technical challenges +- Figuring out the MVP that we can build upon +- Resource constraints] + +# Appendix +[Include any additional information: +- Research findings +- Technical specifications] +</PRD> \ No newline at end of file diff --git a/website-monitoring-frontend/CLAUDE.md b/website-monitoring-frontend/CLAUDE.md new file mode 100644 index 0000000..df6ac5c --- /dev/null +++ b/website-monitoring-frontend/CLAUDE.md @@ -0,0 +1,68 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +- `npm run dev` - Start Next.js development server with Turbopack +- `npm run build` - Build the application for production +- `npm run start` - Start production server +- `npm run lint` - Run ESLint for code quality checks +- `npm run dev:all` - Start both frontend and backend servers concurrently + +## Architecture Overview + +This is a website monitoring platform built with Next.js 15 (App Router) that analyzes website performance, SEO, and accessibility using Lighthouse scans. + +### Core Technologies +- **Frontend**: Next.js 15 with App Router, React 19, TypeScript +- **UI**: Tailwind CSS with Shadcn/UI components +- **Database**: Supabase (PostgreSQL) +- **Authentication**: Supabase Auth +- **Charts**: Recharts and Chart.js +- **Web Scraping**: Puppeteer and Lighthouse integration +- **Backend Worker**: Dockerized Express server for scan processing + +### Project Structure + +- `src/app/` - Next.js App Router pages and API routes + - `api/` - Backend API endpoints for scanning, crawling, analysis + - `dashboard/` - Main dashboard pages and website management + - `auth/` - Authentication pages +- `src/components/` - React components organized by feature + - `core/` - Core dashboard and competitor analysis components + - `dashboard/` - Website monitoring and metrics components + - `ui/` - Reusable UI components (forms, feedback, data display) +- `src/contexts/` - React contexts for auth and monitoring state +- `src/services/` - Business logic services for API calls +- `src/types/` - TypeScript type definitions +- `scanner-worker/` - Dockerized Express worker for Lighthouse scans + +### Key Patterns + +**Authentication Flow**: +- Uses Supabase auth with custom AuthContext (`src/contexts/AuthContext.tsx`) +- Users are automatically assigned to organizations +- Route protection implemented in AuthContext + +**Data Flow**: +- API routes in `src/app/api/` handle backend logic +- Services in `src/services/` manage API calls from frontend +- Supabase client configured in `src/lib/supabase.ts` + +**Component Structure**: +- UI components use Shadcn/UI patterns with Tailwind CSS +- Dashboard components are organized by feature (dashboard/, monitoring/, etc.) +- Reusable components in `ui/` folder with consistent interfaces + +**Scanning Architecture**: +- Frontend triggers scans via API routes +- Docker worker processes Lighthouse scans with Puppeteer +- Results stored in Supabase and displayed in dashboard + +### Development Notes + +- Uses Supabase for both authentication and database +- Scanner worker runs in Docker with Chromium for consistent Lighthouse results +- Components follow Shadcn/UI patterns and use class-variance-authority for styling +- Form validation uses react-hook-form with Zod schemas \ No newline at end of file diff --git a/website-monitoring-frontend/Dockerfile b/website-monitoring-frontend/Dockerfile new file mode 100644 index 0000000..ffe1832 --- /dev/null +++ b/website-monitoring-frontend/Dockerfile @@ -0,0 +1,16 @@ +FROM node:18 + +WORKDIR /app + +COPY package.json package-lock.json ./ + +# Disable the oxide engine so it falls back to JS +ENV TAILWIND_DISABLE_OXIDE=1 + +RUN npm install + +COPY . . + +RUN npm run build + +CMD ["npm", "run", "start"] diff --git a/website-monitoring-frontend/README.md b/website-monitoring-frontend/README.md new file mode 100644 index 0000000..bea3c3c --- /dev/null +++ b/website-monitoring-frontend/README.md @@ -0,0 +1,52 @@ +# Website Monitoring Platform + +This project is a modern website monitoring platform built with Next.js (App Router) for the frontend and a Dockerized Express-based Lighthouse scan worker for performance, SEO, and accessibility analysis. + +## Features + +- Add and manage websites in a dashboard +- Trigger Lighthouse scans for any website via a button in the dashboard +- View scan results directly in the frontend +- Local development with Docker for the scan worker (Chromium included) +- Modular architecture for future automation, cron jobs, and database integration + +--- + +## Getting Started + +### Prerequisites + +- [Node.js](https://nodejs.org/) (for the frontend) +- [Docker](https://www.docker.com/) (for the scan worker) +- [npm](https://www.npmjs.com/) or [pnpm](https://pnpm.io/) (for dependency management) + +--- + +### 1. Install Dependencies + +```bash +cd website-monitoring-frontend +npm install +``` + +### 2. Start the Lighthouse Scan Worker (Docker) + +Build and run the scan worker container (from the project root): + +```bash +docker-compose up --build scan-worker +``` + +This will: + +- Build the worker image (installs Node.js dependencies and Chromium) +- Start the Express server on port 5001 inside the container + +### 3. Start the Next.js Frontend + +In a separate terminal: + +```bash +cd website-monitoring-frontend +npm run dev +``` diff --git a/website-monitoring-frontend/components.json b/website-monitoring-frontend/components.json new file mode 100644 index 0000000..0f21f26 --- /dev/null +++ b/website-monitoring-frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/website-monitoring-frontend/crawl-progress-migration.sql b/website-monitoring-frontend/crawl-progress-migration.sql new file mode 100644 index 0000000..4947b69 --- /dev/null +++ b/website-monitoring-frontend/crawl-progress-migration.sql @@ -0,0 +1,21 @@ +-- Add new columns for improved crawl progress tracking +ALTER TABLE crawl_sessions +ADD COLUMN IF NOT EXISTS total_urls INTEGER DEFAULT 0, +ADD COLUMN IF NOT EXISTS processed_urls INTEGER DEFAULT 0, +ADD COLUMN IF NOT EXISTS progress_percentage INTEGER DEFAULT 0, +ADD COLUMN IF NOT EXISTS current_url TEXT; + +-- Update existing sessions to use new columns (map old data) +UPDATE crawl_sessions +SET + total_urls = pages_discovered, + processed_urls = pages_processed, + progress_percentage = CASE + WHEN pages_discovered > 0 THEN ROUND((pages_processed::float / pages_discovered::float) * 100) + ELSE 0 + END +WHERE total_urls IS NULL OR total_urls = 0; + +-- Add index for better performance +CREATE INDEX IF NOT EXISTS idx_crawl_sessions_status ON crawl_sessions(status); +CREATE INDEX IF NOT EXISTS idx_crawl_sessions_website_status ON crawl_sessions(website_id, status); \ No newline at end of file diff --git a/website-monitoring-frontend/database-fixes.sql b/website-monitoring-frontend/database-fixes.sql new file mode 100644 index 0000000..704336a --- /dev/null +++ b/website-monitoring-frontend/database-fixes.sql @@ -0,0 +1,80 @@ +-- Database Fixes for Website Monitoring Frontend +-- Run this in your Supabase SQL editor to fix the console errors + +-- 1. Add missing columns to crawl_sessions table that the API expects +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS total_urls INTEGER DEFAULT 0; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS processed_urls INTEGER DEFAULT 0; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS progress_percentage INTEGER DEFAULT 0; + +-- 2. Ensure all required columns exist in crawl_sessions table +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS pages_discovered INTEGER DEFAULT 0; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS pages_processed INTEGER DEFAULT 0; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS current_url VARCHAR; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS error_message TEXT; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb; + +-- 3. Add missing columns to users table if they don't exist +ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(); +ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ; +ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR DEFAULT 'member'; + +-- 4. Add missing columns to organizations table if they don't exist +ALTER TABLE organizations ADD COLUMN IF NOT EXISTS api_key TEXT; +ALTER TABLE organizations ADD COLUMN IF NOT EXISTS max_websites INTEGER DEFAULT 10; +ALTER TABLE organizations ADD COLUMN IF NOT EXISTS max_scans_per_month INTEGER DEFAULT 1000; +ALTER TABLE organizations ADD COLUMN IF NOT EXISTS subscription_status VARCHAR DEFAULT 'active'; + +-- 5. Create indexes for better performance +CREATE INDEX IF NOT EXISTS idx_crawl_sessions_status ON crawl_sessions(status); +CREATE INDEX IF NOT EXISTS idx_crawl_sessions_website_status ON crawl_sessions(website_id, status); +CREATE INDEX IF NOT EXISTS idx_users_organization_id ON users(organization_id); +CREATE INDEX IF NOT EXISTS idx_websites_organization_id ON websites(organization_id); + +-- 6. Ensure RLS policies exist for crawl_sessions +-- Enable RLS if not already enabled +ALTER TABLE crawl_sessions ENABLE ROW LEVEL SECURITY; + +-- Create basic RLS policy for crawl_sessions if it doesn't exist +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE tablename = 'crawl_sessions' + AND policyname = 'Allow read for authenticated users' + ) THEN + CREATE POLICY "Allow read for authenticated users" ON crawl_sessions + FOR SELECT USING (true); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE tablename = 'crawl_sessions' + AND policyname = 'Allow insert for authenticated users' + ) THEN + CREATE POLICY "Allow insert for authenticated users" ON crawl_sessions + FOR INSERT WITH CHECK (true); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE tablename = 'crawl_sessions' + AND policyname = 'Allow update for authenticated users' + ) THEN + CREATE POLICY "Allow update for authenticated users" ON crawl_sessions + FOR UPDATE USING (true); + END IF; +END $$; + +-- 7. Refresh Supabase schema cache to pick up new columns +-- This is important to resolve "Could not find column in schema cache" errors +NOTIFY pgrst, 'reload schema'; + +-- 8. Verify the fixes by checking table structure +-- You can run these queries to verify the fixes worked: +-- SELECT column_name, data_type, is_nullable, column_default +-- FROM information_schema.columns +-- WHERE table_name = 'crawl_sessions' +-- ORDER BY ordinal_position; + +COMMIT; + diff --git a/website-monitoring-frontend/database-schema.sql b/website-monitoring-frontend/database-schema.sql new file mode 100644 index 0000000..5c8b827 --- /dev/null +++ b/website-monitoring-frontend/database-schema.sql @@ -0,0 +1,169 @@ +-- Required tables for the website monitoring application +-- Run these in your Supabase SQL editor to create missing tables + +-- Team invitations table +CREATE TABLE IF NOT EXISTS team_invitations ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + email text NOT NULL, + role text CHECK (role IN ('admin', 'member')) NOT NULL DEFAULT 'member', + organization_id uuid REFERENCES organizations(id) ON DELETE CASCADE, + invited_by uuid REFERENCES auth.users(id) ON DELETE SET NULL, + status text CHECK (status IN ('pending', 'accepted', 'expired')) NOT NULL DEFAULT 'pending', + created_at timestamp with time zone DEFAULT now(), + expires_at timestamp with time zone DEFAULT (now() + interval '7 days') +); + +-- User notification preferences table +CREATE TABLE IF NOT EXISTS user_notification_preferences ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE UNIQUE, + email_notifications boolean DEFAULT true, + sms_notifications boolean DEFAULT false, + browser_notifications boolean DEFAULT true, + weekly_report boolean DEFAULT true, + timezone text DEFAULT 'UTC', + date_format text DEFAULT 'MM/DD/YYYY', + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + +-- Alerts table +CREATE TABLE IF NOT EXISTS alerts ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + website_id uuid REFERENCES websites(id) ON DELETE CASCADE, + type text CHECK (type IN ('downtime', 'performance', 'error', 'ssl', 'maintenance')) NOT NULL, + severity text CHECK (severity IN ('low', 'medium', 'high', 'critical')) NOT NULL DEFAULT 'medium', + title text NOT NULL, + message text NOT NULL, + status text CHECK (status IN ('active', 'resolved', 'acknowledged')) NOT NULL DEFAULT 'active', + created_at timestamp with time zone DEFAULT now(), + resolved_at timestamp with time zone, + acknowledged_at timestamp with time zone +); + +-- Alert rules table +CREATE TABLE IF NOT EXISTS alert_rules ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + organization_id uuid REFERENCES organizations(id) ON DELETE CASCADE, + name text NOT NULL, + type text CHECK (type IN ('downtime', 'performance', 'error_rate')) NOT NULL, + condition text NOT NULL, + threshold numeric NOT NULL, + enabled boolean DEFAULT true, + notification_methods text[] DEFAULT ARRAY['email'], + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + +-- Uptime checks table +CREATE TABLE IF NOT EXISTS uptime_checks ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + website_id uuid REFERENCES websites(id) ON DELETE CASCADE, + status text CHECK (status IN ('up', 'down', 'warning')) NOT NULL, + response_time integer, -- in milliseconds + status_code integer, + error_message text, + checked_at timestamp with time zone DEFAULT now() +); + +-- Add missing columns to existing tables if they don't exist +DO $$ +BEGIN + -- Add API key to organizations table + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'organizations' AND column_name = 'api_key') THEN + ALTER TABLE organizations ADD COLUMN api_key text; + END IF; + + -- Add max limits to organizations table + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'organizations' AND column_name = 'max_websites') THEN + ALTER TABLE organizations ADD COLUMN max_websites integer DEFAULT 10; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'organizations' AND column_name = 'max_scans_per_month') THEN + ALTER TABLE organizations ADD COLUMN max_scans_per_month integer DEFAULT 1000; + END IF; + + -- Add created_at to users table if missing + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'created_at') THEN + ALTER TABLE users ADD COLUMN created_at timestamp with time zone DEFAULT now(); + END IF; + + -- Add last_login_at to users table if missing + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'last_login_at') THEN + ALTER TABLE users ADD COLUMN last_login_at timestamp with time zone; + END IF; +END $$; + +-- Create indexes for better performance +CREATE INDEX IF NOT EXISTS idx_team_invitations_organization_id ON team_invitations(organization_id); +CREATE INDEX IF NOT EXISTS idx_team_invitations_email ON team_invitations(email); +CREATE INDEX IF NOT EXISTS idx_alerts_website_id ON alerts(website_id); +CREATE INDEX IF NOT EXISTS idx_alerts_status ON alerts(status); +CREATE INDEX IF NOT EXISTS idx_alert_rules_organization_id ON alert_rules(organization_id); +CREATE INDEX IF NOT EXISTS idx_uptime_checks_website_id ON uptime_checks(website_id); +CREATE INDEX IF NOT EXISTS idx_uptime_checks_checked_at ON uptime_checks(checked_at); + +-- Enable Row Level Security +ALTER TABLE team_invitations ENABLE ROW LEVEL SECURITY; +ALTER TABLE user_notification_preferences ENABLE ROW LEVEL SECURITY; +ALTER TABLE alerts ENABLE ROW LEVEL SECURITY; +ALTER TABLE alert_rules ENABLE ROW LEVEL SECURITY; +ALTER TABLE uptime_checks ENABLE ROW LEVEL SECURITY; + +-- Create RLS policies +-- Team invitations policies +CREATE POLICY "Users can view invitations for their organization" ON team_invitations + FOR SELECT USING ( + organization_id IN ( + SELECT organization_id FROM users WHERE id = auth.uid() + ) + ); + +CREATE POLICY "Admins and owners can manage invitations" ON team_invitations + FOR ALL USING ( + organization_id IN ( + SELECT organization_id FROM users + WHERE id = auth.uid() AND role IN ('admin', 'owner') + ) + ); + +-- User notification preferences policies +CREATE POLICY "Users can manage their own preferences" ON user_notification_preferences + FOR ALL USING (user_id = auth.uid()); + +-- Alerts policies +CREATE POLICY "Users can view alerts for their organization's websites" ON alerts + FOR SELECT USING ( + website_id IN ( + SELECT w.id FROM websites w + JOIN users u ON w.organization_id = u.organization_id + WHERE u.id = auth.uid() + ) + ); + +CREATE POLICY "Users can update alerts for their organization's websites" ON alerts + FOR UPDATE USING ( + website_id IN ( + SELECT w.id FROM websites w + JOIN users u ON w.organization_id = u.organization_id + WHERE u.id = auth.uid() + ) + ); + +-- Alert rules policies +CREATE POLICY "Users can manage alert rules for their organization" ON alert_rules + FOR ALL USING ( + organization_id IN ( + SELECT organization_id FROM users WHERE id = auth.uid() + ) + ); + +-- Uptime checks policies +CREATE POLICY "Users can view uptime checks for their organization's websites" ON uptime_checks + FOR SELECT USING ( + website_id IN ( + SELECT w.id FROM websites w + JOIN users u ON w.organization_id = u.organization_id + WHERE u.id = auth.uid() + ) + ); \ No newline at end of file diff --git a/website-monitoring-frontend/databaseStructure.txt b/website-monitoring-frontend/databaseStructure.txt new file mode 100644 index 0000000..ff3c1a0 --- /dev/null +++ b/website-monitoring-frontend/databaseStructure.txt @@ -0,0 +1,994 @@ +-- Enable necessary extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +CREATE EXTENSION IF NOT EXISTS "pg_stat_statements"; +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +------ ENUMS ------ +-- Core enums for status and types +CREATE TYPE scan_status AS ENUM ( + 'pending', + 'queued', + 'running', + 'completed', + 'failed', + 'cancelled' +); + +CREATE TYPE severity_level AS ENUM ( + 'critical', + 'high', + 'medium', + 'low', + 'info' +); + +CREATE TYPE comparison_operator AS ENUM ( + 'less_than', + 'less_than_equal', + 'greater_than', + 'greater_than_equal', + 'equal_to', + 'not_equal_to' +); + +CREATE TYPE metric_category AS ENUM ( + 'performance', + 'seo', + 'accessibility', + 'best_practices', + 'security', + 'pwa' +); + +CREATE TYPE resource_type AS ENUM ( + 'script', + 'stylesheet', + 'image', + 'font', + 'document', + 'media', + 'other' +); + +CREATE TYPE notification_channel AS ENUM ( + 'email', + 'slack', + 'webhook', + 'in_app' +); + +CREATE TYPE subscription_tier AS ENUM ( + 'free', + 'starter', + 'professional', + 'enterprise' +); + +CREATE TYPE user_role AS ENUM ( + 'owner', + 'admin', + 'editor', + 'viewer' +); + +------ CORE TABLES ------ +-- Organizations table +CREATE TABLE organizations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR NOT NULL, + subscription_tier subscription_tier DEFAULT 'free', + subscription_status VARCHAR DEFAULT 'active', + billing_email VARCHAR, + max_websites INTEGER DEFAULT 5, + max_users INTEGER DEFAULT 3, + scan_frequency_minutes INTEGER DEFAULT 60, + settings JSONB DEFAULT '{ + "alert_email_digest": "daily", + "default_scan_depth": 3, + "retention_days": 90, + "enable_competitor_analysis": false + }'::jsonb, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Users table +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email VARCHAR UNIQUE NOT NULL, + name VARCHAR, + organization_id UUID REFERENCES organizations(id), + role user_role DEFAULT 'viewer', + is_active BOOLEAN DEFAULT true, + last_login_at TIMESTAMPTZ, + settings JSONB DEFAULT '{ + "email_notifications": true, + "notification_frequency": "instant", + "dashboard_layout": "default" + }'::jsonb, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Websites table +CREATE TABLE websites ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID REFERENCES organizations(id) NOT NULL, + base_url VARCHAR NOT NULL, + name VARCHAR NOT NULL, + is_active BOOLEAN DEFAULT true, + crawl_settings JSONB DEFAULT '{ + "max_pages": 100, + "max_depth": 3, + "exclude_patterns": [ + "/admin/*", + "/api/*", + "*.pdf", + "*.jpg", + "*.png" + ], + "include_patterns": ["/*"], + "respect_robots_txt": true, + "crawl_frequency": "daily", + "crawl_timing": "off_peak" + }'::jsonb, + scan_schedule JSONB DEFAULT '{ + "frequency": "hourly", + "time_windows": ["0-6", "20-23"], + "days": ["monday", "tuesday", "wednesday", "thursday", "friday"] + }'::jsonb, + performance_budgets JSONB DEFAULT '{ + "page_weight_kb": 1000, + "max_requests": 100, + "time_to_interactive_ms": 3000, + "first_contentful_paint_ms": 1000 + }'::jsonb, + notifications JSONB DEFAULT '{ + "channels": ["email"], + "thresholds": { + "performance": 90, + "accessibility": 90, + "seo": 90, + "best_practices": 90 + } + }'::jsonb, + last_crawl_at TIMESTAMPTZ, + last_scan_at TIMESTAMPTZ, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(organization_id, base_url) +); + +-- Competitor tracking +CREATE TABLE competitor_websites ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + competitor_url VARCHAR NOT NULL, + name VARCHAR NOT NULL, + is_active BOOLEAN DEFAULT true, + scan_frequency VARCHAR DEFAULT 'daily', + metrics_to_track VARCHAR[] DEFAULT ARRAY[ + 'performance', + 'seo', + 'accessibility', + 'best_practices' + ], + last_scan_at TIMESTAMPTZ, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(website_id, competitor_url) +); + +-- Pages table +CREATE TABLE pages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + url VARCHAR NOT NULL, + path VARCHAR NOT NULL, + title VARCHAR, + description TEXT, + content_hash VARCHAR, + content_type VARCHAR, + status_code INTEGER, + is_active BOOLEAN DEFAULT true, + priority INTEGER DEFAULT 1, + depth INTEGER DEFAULT 0, + parent_page_id UUID REFERENCES pages(id), + discovery_method VARCHAR DEFAULT 'crawl', + last_seen_at TIMESTAMPTZ, + metadata JSONB DEFAULT '{ + "inbound_links": 0, + "outbound_links": 0, + "word_count": 0, + "has_canonical": false, + "is_indexable": true + }'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(website_id, url) +); + +------ METRIC DEFINITIONS AND THRESHOLDS ------ +CREATE TABLE metric_definitions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + key VARCHAR NOT NULL UNIQUE, + name VARCHAR NOT NULL, + description TEXT NOT NULL, + category metric_category NOT NULL, + unit VARCHAR, + is_core_metric BOOLEAN DEFAULT false, + default_threshold NUMERIC, + warning_threshold NUMERIC, + critical_threshold NUMERIC, + direction VARCHAR NOT NULL DEFAULT 'higher_is_better', + weight NUMERIC DEFAULT 1.0, + documentation_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Populate core metrics +INSERT INTO metric_definitions +(key, name, description, category, unit, is_core_metric, default_threshold, warning_threshold, critical_threshold, direction) +VALUES +-- Core Web Vitals +('performance', 'Performance Score', 'Overall performance score of the website', 'performance', '%', true, 90, 80, 70, 'higher_is_better'), +('accessibility', 'Accessibility Score', 'Overall accessibility score of the website', 'accessibility', '%', true, 90, 80, 70, 'higher_is_better'), +('seo', 'SEO Score', 'Overall SEO score of the website', 'seo', '%', true, 90, 80, 70, 'higher_is_better'), +('bestPractices', 'Best Practices Score', 'Overall best practices score', 'best_practices', '%', true, 90, 80, 70, 'higher_is_better'), + +-- Performance Metrics +('firstContentfulPaint', 'First Contentful Paint', 'Time when the first text or image is painted', 'performance', 'ms', true, 1800, 2500, 4000, 'lower_is_better'), +('largestContentfulPaint', 'Largest Contentful Paint', 'Time when the largest text or image is painted', 'performance', 'ms', true, 2500, 4000, 6000, 'lower_is_better'), +('totalBlockingTime', 'Total Blocking Time', 'Sum of all time periods between FCP and Time to Interactive', 'performance', 'ms', true, 200, 400, 600, 'lower_is_better'), +('cumulativeLayoutShift', 'Cumulative Layout Shift', 'Measures visual stability', 'performance', 'score', true, 0.1, 0.25, 0.4, 'lower_is_better'), +('speedIndex', 'Speed Index', 'How quickly content is visually displayed', 'performance', 'ms', true, 3400, 5800, 8800, 'lower_is_better'), +('interactive', 'Time to Interactive', 'Time to fully interactive', 'performance', 'ms', true, 3800, 7300, 12700, 'lower_is_better'), + +-- Resource Metrics +('totalByteWeight', 'Total Byte Weight', 'Total size of all resources', 'performance', 'bytes', false, 1600000, 2400000, 3200000, 'lower_is_better'), +('serverResponseTime', 'Server Response Time', 'Time for server to respond to main document request', 'performance', 'ms', false, 100, 200, 400, 'lower_is_better'), +('networkRtt', 'Network Round Trip Time', 'Network round trip time', 'performance', 'ms', false, 40, 100, 150, 'lower_is_better'), +('networkServerLatency', 'Network Server Latency', 'Server latency in network requests', 'performance', 'ms', false, 30, 100, 150, 'lower_is_better'); + +------ SCANS AND RESULTS ------ +-- Scans table +CREATE TABLE scans ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + page_id UUID REFERENCES pages(id) NOT NULL, + triggered_by UUID REFERENCES users(id), + scan_type VARCHAR NOT NULL DEFAULT 'full', + status scan_status DEFAULT 'pending', + priority INTEGER DEFAULT 1, + categories metric_category[] DEFAULT ARRAY['performance', 'seo', 'accessibility', 'best_practices'], + device_type VARCHAR DEFAULT 'desktop', + user_agent VARCHAR, + lighthouse_version VARCHAR, + chrome_version VARCHAR, + environment JSONB DEFAULT '{}'::jsonb, + started_at TIMESTAMPTZ DEFAULT NOW(), + completed_at TIMESTAMPTZ, + duration_ms INTEGER, + error_message TEXT, + retry_count INTEGER DEFAULT 0, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Scan results +CREATE TABLE scan_results ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + scan_id UUID REFERENCES scans(id) NOT NULL, + category metric_category NOT NULL, + score NUMERIC, + raw_data JSONB, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Metric values +CREATE TABLE metric_values ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + scan_id UUID REFERENCES scans(id) NOT NULL, + metric_id UUID REFERENCES metric_definitions(id) NOT NULL, + value NUMERIC NOT NULL, + raw_value VARCHAR, + unit VARCHAR, + is_passing BOOLEAN, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Resource analysis +CREATE TABLE resource_analysis ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + scan_id UUID REFERENCES scans(id) NOT NULL, + resource_type resource_type NOT NULL, + url VARCHAR NOT NULL, + size_bytes INTEGER NOT NULL, + transfer_size_bytes INTEGER, + duration_ms INTEGER, + is_third_party BOOLEAN DEFAULT false, + is_cached BOOLEAN, + compression_ratio NUMERIC, + mime_type VARCHAR, + protocol VARCHAR, + priority VARCHAR, + status_code INTEGER, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +------ MONITORING AND ALERTS ------ +-- Alert configurations +CREATE TABLE alert_configurations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + name VARCHAR NOT NULL, + description TEXT, + metric_id UUID REFERENCES metric_definitions(id) NOT NULL, + threshold NUMERIC NOT NULL, + comparison comparison_operator DEFAULT 'less_than', + severity severity_level DEFAULT 'medium', + consecutive_count INTEGER DEFAULT 1, + cooldown_minutes INTEGER DEFAULT 60, + notification_channels notification_channel[] DEFAULT ARRAY['email'], + notification_template TEXT, + is_active BOOLEAN DEFAULT true, + last_triggered_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Alerts +CREATE TABLE alerts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + page_id UUID REFERENCES pages(id), + config_id UUID REFERENCES alert_configurations(id), + metric_id UUID REFERENCES metric_definitions(id), + severity severity_level DEFAULT 'medium', + title VARCHAR NOT NULL, + message TEXT NOT NULL, + details JSONB DEFAULT '{}'::jsonb, + status VARCHAR DEFAULT 'open', + acknowledged_by UUID REFERENCES users(id), + acknowledged_at TIMESTAMPTZ, + resolved_at TIMESTAMPTZ, + resolution_note TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Alert history +CREATE TABLE alert_history ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + alert_id UUID REFERENCES alerts(id) NOT NULL, + event_type VARCHAR NOT NULL, + event_data JSONB NOT NULL, + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Notification delivery +CREATE TABLE notification_deliveries ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + alert_id UUID REFERENCES alerts(id) NOT NULL, + channel notification_channel NOT NULL, + recipient VARCHAR NOT NULL, + content TEXT NOT NULL, + status VARCHAR DEFAULT 'pending', + sent_at TIMESTAMPTZ, + error_message TEXT, + retry_count INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +------ CRAWL MANAGEMENT ------ +-- Crawl queue +CREATE TABLE crawl_queue ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + url VARCHAR NOT NULL, + priority INTEGER DEFAULT 1, + status VARCHAR DEFAULT 'pending', + parent_url VARCHAR, + discovery_depth INTEGER DEFAULT 0, + attempts INTEGER DEFAULT 0, + last_attempt_at TIMESTAMPTZ, + next_attempt_at TIMESTAMPTZ, + error_message TEXT, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Crawl sessions +CREATE TABLE crawl_sessions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + status VARCHAR DEFAULT 'running', + pages_discovered INTEGER DEFAULT 0, + pages_processed INTEGER DEFAULT 0, + start_url VARCHAR NOT NULL, + max_depth INTEGER, + started_at TIMESTAMPTZ DEFAULT NOW(), + completed_at TIMESTAMPTZ, + error_message TEXT, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- URL patterns +CREATE TABLE url_patterns ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + pattern VARCHAR NOT NULL, + pattern_type VARCHAR NOT NULL, -- 'include' or 'exclude' + description TEXT, + is_regex BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + priority INTEGER DEFAULT 1, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +------ PERFORMANCE BUDGETS ------ +CREATE TABLE performance_budgets ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + name VARCHAR NOT NULL, + description TEXT, + metric_id UUID REFERENCES metric_definitions(id), + budget_type VARCHAR NOT NULL, -- 'size', 'timing', 'count' + threshold NUMERIC NOT NULL, + applies_to JSONB DEFAULT '{ + "resource_types": ["all"], + "paths": ["/*"] + }'::jsonb, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Budget violations +CREATE TABLE budget_violations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + budget_id UUID REFERENCES performance_budgets(id) NOT NULL, + scan_id UUID REFERENCES scans(id) NOT NULL, + actual_value NUMERIC NOT NULL, + threshold_value NUMERIC NOT NULL, + percentage_over NUMERIC, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +------ CUSTOM DASHBOARDS AND REPORTS ------ +-- Dashboard definitions +CREATE TABLE dashboards ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID REFERENCES organizations(id) NOT NULL, + name VARCHAR NOT NULL, + description TEXT, + layout JSONB DEFAULT '[]'::jsonb, + is_default BOOLEAN DEFAULT false, + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Dashboard widgets +CREATE TABLE dashboard_widgets ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + dashboard_id UUID REFERENCES dashboards(id) NOT NULL, + widget_type VARCHAR NOT NULL, + name VARCHAR NOT NULL, + config JSONB NOT NULL, + position JSONB NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Report templates +CREATE TABLE report_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID REFERENCES organizations(id) NOT NULL, + name VARCHAR NOT NULL, + description TEXT, + template_type VARCHAR NOT NULL, + content JSONB NOT NULL, + schedule JSONB DEFAULT NULL, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Generated reports +CREATE TABLE generated_reports ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + template_id UUID REFERENCES report_templates(id) NOT NULL, + website_id UUID REFERENCES websites(id), + generated_by UUID REFERENCES users(id), + report_data JSONB NOT NULL, + format VARCHAR DEFAULT 'pdf', + file_url VARCHAR, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +------ API ACCESS AND RATE LIMITING ------ +-- API keys +CREATE TABLE api_keys ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID REFERENCES organizations(id) NOT NULL, + name VARCHAR NOT NULL, + key_hash VARCHAR NOT NULL, + scopes VARCHAR[] DEFAULT ARRAY['read'], + rate_limit_per_minute INTEGER DEFAULT 60, + is_active BOOLEAN DEFAULT true, + last_used_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ, + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Rate limiting +CREATE TABLE rate_limits ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + key_type VARCHAR NOT NULL, -- 'api_key', 'ip_address' + key_value VARCHAR NOT NULL, + window_start TIMESTAMPTZ NOT NULL, + request_count INTEGER DEFAULT 1, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +------ AUDIT LOGGING ------ +CREATE TABLE audit_logs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID REFERENCES organizations(id), + user_id UUID REFERENCES users(id), + action VARCHAR NOT NULL, + entity_type VARCHAR NOT NULL, + entity_id UUID, + changes JSONB, + ip_address VARCHAR, + user_agent VARCHAR, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +------ VIEWS ------ +-- Performance trend view +CREATE VIEW performance_trends AS +SELECT + w.id AS website_id, + w.name AS website_name, + p.url AS page_url, + m.key AS metric_key, + mv.value AS metric_value, + s.created_at AS scan_date, + AVG(mv.value) OVER ( + PARTITION BY w.id, p.id, m.id + ORDER BY s.created_at + ROWS BETWEEN 6 PRECEDING AND CURRENT ROW + ) AS rolling_average +FROM websites w +JOIN pages p ON p.website_id = w.id +JOIN scans s ON s.page_id = p.id +JOIN metric_values mv ON mv.scan_id = s.id +JOIN metric_definitions m ON m.id = mv.metric_id +WHERE s.created_at >= NOW() - INTERVAL '30 days'; + +-- Resource usage summary view +CREATE VIEW resource_usage_summary AS +SELECT + w.id AS website_id, + w.name AS website_name, + ra.resource_type, + COUNT(*) AS resource_count, + AVG(ra.size_bytes) AS avg_size, + SUM(ra.size_bytes) AS total_size, + AVG(ra.duration_ms) AS avg_duration +FROM websites w +JOIN scans s ON s.website_id = w.id +JOIN resource_analysis ra ON ra.scan_id = s.id +WHERE s.created_at >= NOW() - INTERVAL '24 hours' +GROUP BY w.id, w.name, ra.resource_type; + +------ FUNCTIONS ------ +-- Calculate health score +CREATE OR REPLACE FUNCTION calculate_health_score(website_id UUID) +RETURNS NUMERIC AS $$ +DECLARE + score NUMERIC; +BEGIN + SELECT + AVG( + CASE + WHEN m.direction = 'higher_is_better' THEN + LEAST(mv.value / NULLIF(m.default_threshold, 0) * 100, 100) + ELSE + LEAST(m.default_threshold / NULLIF(mv.value, 0) * 100, 100) + END + ) + INTO score + FROM scans s + JOIN metric_values mv ON mv.scan_id = s.id + JOIN metric_definitions m ON m.id = mv.metric_id + WHERE s.website_id = calculate_health_score.website_id + AND s.created_at >= NOW() - INTERVAL '24 hours' + AND m.is_core_metric = true; + + RETURN COALESCE(score, 0); +END; +$$ LANGUAGE plpgsql; + +-- Update scan status +CREATE OR REPLACE FUNCTION update_scan_status() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.status = 'completed' THEN + -- Update website's last scan timestamp + UPDATE websites + SET last_scan_at = NOW() + WHERE id = NEW.website_id; + + -- Check for alerts + INSERT INTO alerts (website_id, page_id, severity, title, message) + SELECT + NEW.website_id, + NEW.page_id, + ac.severity, + 'Metric threshold exceeded', + format('%s is %s threshold of %s', m.name, ac.comparison, ac.threshold) + FROM metric_values mv + JOIN metric_definitions m ON m.id = mv.metric_id + JOIN alert_configurations ac ON ac.metric_id = m.id + WHERE mv.scan_id = NEW.id + AND ac.website_id = NEW.website_id + AND ( + CASE ac.comparison + WHEN 'less_than' THEN mv.value < ac.threshold + WHEN 'greater_than' THEN mv.value > ac.threshold + WHEN 'equal_to' THEN mv.value = ac.threshold + END + ); + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create trigger for scan status updates +CREATE TRIGGER scan_status_update + AFTER UPDATE OF status ON scans + FOR EACH ROW + WHEN (OLD.status IS DISTINCT FROM NEW.status) + EXECUTE FUNCTION update_scan_status(); + + ------ INDEXES ------ +-- Performance indexes +CREATE INDEX idx_scans_website_status ON scans(website_id, status); +CREATE INDEX idx_scans_created_at ON scans(created_at); +CREATE INDEX idx_metric_values_scan_metric ON metric_values(scan_id, metric_id); +CREATE INDEX idx_pages_website_active ON pages(website_id, is_active); +CREATE INDEX idx_crawl_queue_status_priority ON crawl_queue(status, priority); +CREATE INDEX idx_alerts_website_status ON alerts(website_id, status); +CREATE INDEX idx_resource_analysis_scan ON resource_analysis(scan_id); +CREATE INDEX idx_audit_logs_organization ON audit_logs(organization_id, created_at); +CREATE INDEX idx_metric_values_created_at ON metric_values(created_at); +CREATE INDEX idx_pages_url_trgm ON pages USING gin (url gin_trgm_ops); + +------ SECURITY POLICIES ------ +-- RLS Policies +ALTER TABLE organizations ENABLE ROW LEVEL SECURITY; +ALTER TABLE users ENABLE ROW LEVEL SECURITY; +ALTER TABLE websites ENABLE ROW LEVEL SECURITY; +ALTER TABLE pages ENABLE ROW LEVEL SECURITY; +ALTER TABLE scans ENABLE ROW LEVEL SECURITY; +ALTER TABLE metric_values ENABLE ROW LEVEL SECURITY; +ALTER TABLE alerts ENABLE ROW LEVEL SECURITY; +ALTER TABLE dashboards ENABLE ROW LEVEL SECURITY; +ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY; + +-- Organization access +CREATE POLICY "Users can view their organization" + ON organizations + FOR SELECT + USING (id IN ( + SELECT organization_id FROM users WHERE id = auth.uid() + )); + +-- Website access +CREATE POLICY "Users can view their organization's websites" + ON websites + FOR SELECT + USING (organization_id IN ( + SELECT organization_id FROM users WHERE id = auth.uid() + )); + +CREATE POLICY "Admins can manage their organization's websites" + ON websites + FOR ALL + USING ( + organization_id IN ( + SELECT organization_id + FROM users + WHERE id = auth.uid() AND role IN ('admin', 'owner') + ) + ); + +------ DATA RETENTION ------ +-- Create retention policy function +CREATE OR REPLACE FUNCTION apply_data_retention() +RETURNS void AS $$ +DECLARE + org RECORD; +BEGIN + -- Loop through organizations + FOR org IN SELECT id, (settings->>'retention_days')::integer AS retention_days + FROM organizations + WHERE settings->>'retention_days' IS NOT NULL + LOOP + -- Delete old scan data + DELETE FROM metric_values + WHERE scan_id IN ( + SELECT id FROM scans + WHERE website_id IN ( + SELECT id FROM websites WHERE organization_id = org.id + ) + AND created_at < NOW() - (org.retention_days || ' days')::interval + ); + + -- Delete old resource analysis + DELETE FROM resource_analysis + WHERE scan_id IN ( + SELECT id FROM scans + WHERE website_id IN ( + SELECT id FROM websites WHERE organization_id = org.id + ) + AND created_at < NOW() - (org.retention_days || ' days')::interval + ); + + -- Delete old scans + DELETE FROM scans + WHERE website_id IN ( + SELECT id FROM websites WHERE organization_id = org.id + ) + AND created_at < NOW() - (org.retention_days || ' days')::interval; + + -- Archive resolved alerts + UPDATE alerts + SET status = 'archived' + WHERE website_id IN ( + SELECT id FROM websites WHERE organization_id = org.id + ) + AND status = 'resolved' + AND resolved_at < NOW() - (org.retention_days || ' days')::interval; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +------ ADDITIONAL FUNCTIONS ------ +-- Calculate competitor comparison +CREATE OR REPLACE FUNCTION calculate_competitor_comparison(website_id UUID) +RETURNS TABLE ( + metric_key VARCHAR, + your_score NUMERIC, + competitor_avg NUMERIC, + competitor_best NUMERIC, + percentile NUMERIC +) AS $$ +BEGIN + RETURN QUERY + WITH your_metrics AS ( + SELECT + m.key, + mv.value + FROM scans s + JOIN metric_values mv ON mv.scan_id = s.id + JOIN metric_definitions m ON m.id = mv.metric_id + WHERE s.website_id = calculate_competitor_comparison.website_id + AND s.created_at = ( + SELECT MAX(created_at) + FROM scans + WHERE website_id = calculate_competitor_comparison.website_id + ) + ), + competitor_metrics AS ( + SELECT + m.key, + mv.value, + PERCENT_RANK() OVER (PARTITION BY m.key ORDER BY mv.value) as percentile + FROM scans s + JOIN metric_values mv ON mv.scan_id = s.id + JOIN metric_definitions m ON m.id = mv.metric_id + WHERE s.website_id IN ( + SELECT competitor_url_id + FROM competitor_websites + WHERE website_id = calculate_competitor_comparison.website_id + ) + AND s.created_at >= NOW() - INTERVAL '30 days' + ) + SELECT + ym.key, + ym.value as your_score, + AVG(cm.value) as competitor_avg, + MAX(cm.value) as competitor_best, + MAX(cm.percentile) * 100 as percentile + FROM your_metrics ym + LEFT JOIN competitor_metrics cm ON cm.key = ym.key + GROUP BY ym.key, ym.value; +END; +$$ LANGUAGE plpgsql; + +-- Generate performance report +CREATE OR REPLACE FUNCTION generate_performance_report(website_id UUID, days INTEGER) +RETURNS JSONB AS $$ +DECLARE + report JSONB; +BEGIN + SELECT jsonb_build_object( + 'website_info', ( + SELECT jsonb_build_object( + 'name', name, + 'url', base_url, + 'report_period', jsonb_build_object( + 'start', NOW() - (days || ' days')::interval, + 'end', NOW() + ) + ) + FROM websites + WHERE id = website_id + ), + 'performance_summary', ( + SELECT jsonb_build_object( + 'average_performance_score', AVG(mv.value), + 'best_performance_score', MAX(mv.value), + 'worst_performance_score', MIN(mv.value), + 'trend', jsonb_agg( + jsonb_build_object( + 'date', DATE(s.created_at), + 'score', mv.value + ) ORDER BY s.created_at + ) + ) + FROM scans s + JOIN metric_values mv ON mv.scan_id = s.id + JOIN metric_definitions md ON md.id = mv.metric_id + WHERE s.website_id = generate_performance_report.website_id + AND md.key = 'performance' + AND s.created_at >= NOW() - (days || ' days')::interval + ), + 'core_metrics', ( + SELECT jsonb_object_agg( + md.key, + jsonb_build_object( + 'average', AVG(mv.value), + 'best', MAX(mv.value), + 'worst', MIN(mv.value) + ) + ) + FROM scans s + JOIN metric_values mv ON mv.scan_id = s.id + JOIN metric_definitions md ON md.id = mv.metric_id + WHERE s.website_id = generate_performance_report.website_id + AND md.is_core_metric = true + AND s.created_at >= NOW() - (days || ' days')::interval + ), + 'resource_summary', ( + SELECT jsonb_object_agg( + resource_type, + jsonb_build_object( + 'count', COUNT(*), + 'total_size', SUM(size_bytes), + 'average_duration', AVG(duration_ms) + ) + ) + FROM scans s + JOIN resource_analysis ra ON ra.scan_id = s.id + WHERE s.website_id = generate_performance_report.website_id + AND s.created_at >= NOW() - (days || ' days')::interval + ), + 'alerts', ( + SELECT jsonb_agg( + jsonb_build_object( + 'severity', severity, + 'message', message, + 'created_at', created_at + ) + ) + FROM alerts + WHERE website_id = generate_performance_report.website_id + AND created_at >= NOW() - (days || ' days')::interval + ) + ) INTO report; + + RETURN report; +END; +$$ LANGUAGE plpgsql; + +------ NOTIFICATIONS ------ +-- Create notification function +CREATE OR REPLACE FUNCTION process_alert_notifications() +RETURNS trigger AS $$ +DECLARE + website_record RECORD; + user_record RECORD; +BEGIN + -- Get website details + SELECT * INTO website_record + FROM websites + WHERE id = NEW.website_id; + + -- Insert notification for each user in the organization + FOR user_record IN + SELECT u.* + FROM users u + WHERE u.organization_id = website_record.organization_id + AND (u.settings->>'email_notifications')::boolean = true + LOOP + INSERT INTO notification_deliveries ( + alert_id, + channel, + recipient, + content + ) VALUES ( + NEW.id, + 'email', + user_record.email, + format( + 'Alert for %s: %s. Severity: %s', + website_record.name, + NEW.message, + NEW.severity + ) + ); + END LOOP; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create trigger for alert notifications +CREATE TRIGGER alert_notification_trigger + AFTER INSERT ON alerts + FOR EACH ROW + EXECUTE FUNCTION process_alert_notifications(); + +------ MAINTENANCE PROCEDURES ------ +-- Create maintenance function +CREATE OR REPLACE FUNCTION perform_maintenance() +RETURNS void AS $$ +BEGIN + -- Clean up old rate limit records + DELETE FROM rate_limits + WHERE window_start < NOW() - INTERVAL '1 day'; + + -- Archive old notifications + UPDATE notification_deliveries + SET status = 'archived' + WHERE created_at < NOW() - INTERVAL '30 days'; + + -- Clean up expired API keys + UPDATE api_keys + SET is_active = false + WHERE expires_at < NOW(); + + -- Update statistics + ANALYZE websites; + ANALYZE scans; + ANALYZE metric_values; + ANALYZE resource_analysis; + + -- Vacuum analyze for better query planning + VACUUM ANALYZE websites; + VACUUM ANALYZE scans; + VACUUM ANALYZE metric_values; + VACUUM ANALYZE resource_analysis; +END; +$$ LANGUAGE plpgsql; diff --git a/website-monitoring-frontend/deploy-schema.js b/website-monitoring-frontend/deploy-schema.js new file mode 100644 index 0000000..966c7e4 --- /dev/null +++ b/website-monitoring-frontend/deploy-schema.js @@ -0,0 +1,67 @@ +const fs = require('fs'); +const { createClient } = require('@supabase/supabase-js'); + +// Read environment variables +require('dotenv').config(); + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + +if (!supabaseUrl || !serviceRoleKey) { + console.error('Missing required environment variables'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, serviceRoleKey, { + auth: { + autoRefreshToken: false, + persistSession: false + } +}); + +async function deploySchema() { + try { + console.log('🚀 Starting database schema deployment...'); + + // Read the SQL file + const sqlContent = fs.readFileSync('supabase-fixes.sql', 'utf8'); + + // Split the SQL into individual statements + const statements = sqlContent + .split(';') + .map(stmt => stmt.trim()) + .filter(stmt => stmt.length > 0 && !stmt.startsWith('--')); + + console.log(`📝 Found ${statements.length} SQL statements to execute`); + + // Execute each statement + for (let i = 0; i < statements.length; i++) { + const statement = statements[i]; + if (statement.trim()) { + console.log(`⚡ Executing statement ${i + 1}/${statements.length}...`); + + try { + const { data, error } = await supabase.rpc('exec_sql', { + sql: statement + ';' + }); + + if (error) { + console.warn(`⚠️ Warning on statement ${i + 1}:`, error.message); + } else { + console.log(`✅ Statement ${i + 1} executed successfully`); + } + } catch (err) { + console.warn(`⚠️ Error on statement ${i + 1}:`, err.message); + } + } + } + + console.log('🎉 Database schema deployment completed!'); + + } catch (error) { + console.error('❌ Error deploying schema:', error); + process.exit(1); + } +} + +deploySchema(); diff --git a/website-monitoring-frontend/devenv.lock b/website-monitoring-frontend/devenv.lock new file mode 100644 index 0000000..4e81ee2 --- /dev/null +++ b/website-monitoring-frontend/devenv.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1754344171, + "owner": "cachix", + "repo": "devenv", + "rev": "03e3a284d2e16e5aaced317cf84dfb392470ca6e", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1750779888, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1753719760, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "0f871fffdc0e5852ec25af99ea5f09ca7be9b632", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + } + }, + "root": "root", + "version": 7 +} diff --git a/website-monitoring-frontend/devenv.nix b/website-monitoring-frontend/devenv.nix new file mode 100644 index 0000000..fecddd5 --- /dev/null +++ b/website-monitoring-frontend/devenv.nix @@ -0,0 +1,66 @@ +{ pkgs, ... }: { + packages = with pkgs; [ + # Node.js and package managers + nodejs_20 + yarn + nodePackages.typescript + nodePackages.typescript-language-server + + # Database + postgresql_15 + + # Development tools + git + curl + jq + + # Optional: Add Docker if you want to manage Docker services + # docker + # docker-compose + ]; + + # PostgreSQL service for local development + services.postgres = { + enable = true; + package = pkgs.postgresql_15; + initialDatabases = [{ name = "website_monitoring"; }]; + initialScript = '' + CREATE USER website_monitoring WITH PASSWORD 'password'; + GRANT ALL PRIVILEGES ON DATABASE website_monitoring TO website_monitoring; + ''; + }; + + # Optional: Add Redis if needed for caching/sessions + # services.redis.enable = true; + + # Environment variables + env.POSTGRES_DB = "website_monitoring"; + env.POSTGRES_USER = "website_monitoring"; + env.POSTGRES_PASSWORD = "password"; + env.DATABASE_URL = "postgresql://website_monitoring:password@localhost:5432/website_monitoring"; + + # Scripts that run when entering the environment + enterShell = '' + echo "🚀 Website Monitoring Frontend Development Environment" + echo "📦 Node.js $(node --version)" + echo "🐘 PostgreSQL $(psql --version)" + echo "" + echo "Available commands:" + echo " npm run dev - Start development server" + echo " npm run build - Build for production" + echo " npm run lint - Run ESLint" + echo " psql - Connect to PostgreSQL" + echo "" + echo "Database connection:" + echo " Host: localhost" + echo " Port: 5432" + echo " Database: website_monitoring" + echo " User: website_monitoring" + echo " Password: password" + echo "" + ''; + + # Pre-commit hooks (optional) + # pre-commit.hooks.shellcheck.enable = true; + # pre-commit.hooks.nixpkgs-fmt.enable = true; +} diff --git a/website-monitoring-frontend/devenv.yaml b/website-monitoring-frontend/devenv.yaml new file mode 100644 index 0000000..116a2ad --- /dev/null +++ b/website-monitoring-frontend/devenv.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling + +# If you're using non-OSS software, you can set allowUnfree to true. +# allowUnfree: true + +# If you're willing to use a package that's vulnerable +# permittedInsecurePackages: +# - "openssl-1.1.1w" + +# If you have more than one devenv you can merge them +#imports: +# - ./backend diff --git a/website-monitoring-frontend/docker-compose.yml b/website-monitoring-frontend/docker-compose.yml new file mode 100644 index 0000000..6fdc3ad --- /dev/null +++ b/website-monitoring-frontend/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.8" +services: + frontend: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + env_file: + - .env + + scan-worker: + build: + context: ./scanner-worker + dockerfile: Dockerfile + ports: + - "5001:5001" + restart: unless-stopped diff --git a/website-monitoring-frontend/docs/automatic-scanning-system.md b/website-monitoring-frontend/docs/automatic-scanning-system.md new file mode 100644 index 0000000..4bc1bf4 --- /dev/null +++ b/website-monitoring-frontend/docs/automatic-scanning-system.md @@ -0,0 +1,261 @@ +# Automatic Lighthouse Scanning System + +This document describes the automatic Lighthouse scanning system that has been integrated into your website monitoring application. + +## Overview + +The automatic scanning system provides: +- **Scheduled Scans**: Periodic scans based on user-configured schedules +- **Change Detection**: Automatic scans triggered when website content changes +- **Subscription Limits**: Respects user subscription tiers and rate limits +- **Webhook Support**: External triggers for website changes +- **Comprehensive UI**: User-friendly interface for managing scan schedules + +## System Architecture + +### Core Components + +1. **LighthouseScanner** (`src/services/lighthouseScanner.ts`) + - Handles core scanning logic + - Manages change detection + - Enforces subscription limits + - Simulates Lighthouse scans + +2. **ScanScheduler** (`src/services/scanScheduler.ts`) + - Manages scheduled scans + - Processes change detection + - Orchestrates scan execution + +3. **Cron Handler** (`src/app/api/cron/scan/route.ts`) + - Main entry point for automated scans + - Supports different scan modes + - Provides scan statistics + +4. **Webhook Handler** (`src/app/api/webhooks/website-change/route.ts`) + - Receives external change notifications + - Triggers high-priority scans + - Validates subscription limits + +5. **ScanScheduleManager** (`src/components/dashboard/ScanScheduleManager.tsx`) + - User interface for managing scan schedules + - Displays usage statistics + - Allows manual scan triggers + +## Features + +### Scheduled Scanning +- **Frequency Options**: Hourly, daily, weekly, monthly +- **Device Types**: Desktop and/or mobile +- **Categories**: Performance, accessibility, SEO, best practices +- **Subscription Tiers**: Different limits per tier + +### Change Detection +- **Content Hashing**: Detects changes in website content +- **Automatic Triggers**: High-priority scans when changes detected +- **Subscription Validation**: Only available for certain tiers + +### Subscription Management +- **Daily Limits**: Maximum scans per day +- **Monthly Limits**: Maximum scans per month +- **Feature Access**: Different capabilities per tier +- **Usage Tracking**: Real-time usage monitoring + +### Webhook Integration +- **External Triggers**: Receive change notifications from external systems +- **Validation**: Verify subscription and limits +- **Audit Logging**: Track all webhook activities + +## Database Schema + +The system uses several new tables: + +### Core Tables +- `scans`: Main scan records +- `scan_results`: Detailed scan results +- `pages`: Website pages with content hashes +- `metric_values`: Individual metric values +- `resource_analysis`: Resource usage analysis + +### Configuration Tables +- `metric_definitions`: Available metrics +- `alert_configurations`: Alert settings +- `subscription_limits`: Tier-based limits + +### Audit Tables +- `audit_logs`: System activity logging +- `crawl_queue`: Crawl job queue +- `crawl_sessions`: Crawl session tracking + +## API Endpoints + +### Cron Endpoints +``` +POST /api/cron/scan?mode=all # Full scan (scheduled + change detection) +POST /api/cron/scan?mode=scheduled # Scheduled scans only +POST /api/cron/scan?mode=change_detection # Change detection only +``` + +### Webhook Endpoints +``` +POST /api/webhooks/website-change # External change notifications +``` + +### Manual Endpoints +``` +POST /api/cron/scan # Manual scan trigger (authenticated) +``` + +## Configuration + +### Environment Variables +```env +NEXT_PUBLIC_SUPABASE_URL=your_supabase_url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key +SUPABASE_SERVICE_ROLE_KEY=your_service_role_key +``` + +### Subscription Tiers +- **Free**: 10 scans/day, 100 scans/month +- **Pro**: 50 scans/day, 500 scans/month +- **Enterprise**: 200 scans/day, 2000 scans/month + +## Usage + +### Setting Up Automated Scans + +1. **Deploy the Application** + ```bash + # Deploy to Vercel (recommended) + vercel --prod + + # Or deploy to your preferred platform + ``` + +2. **Set Up Cron Jobs** + ```bash + # Run the setup script + ./scripts/setup-cron.sh + + # Or follow the manual setup guide + # docs/cron-setup-guide.md + ``` + +3. **Configure Database** + ```sql + -- Run the setup script + \i setup-database.sql + ``` + +### Managing Scan Schedules + +1. **Access the Dashboard** + - Navigate to `/dashboard/websites` + - Click on a website to view details + - Find the "Scan Schedule Management" section + +2. **Configure Settings** + - Toggle automatic scanning on/off + - Set scan frequency (hourly, daily, weekly, monthly) + - Choose device types (desktop, mobile) + - Select scan categories + +3. **Monitor Usage** + - View daily and monthly scan usage + - Check against subscription limits + - Trigger manual scans when needed + +### Webhook Integration + +1. **Set Up External Monitoring** + - Configure your external system to detect website changes + - Send POST requests to `/api/webhooks/website-change` + +2. **Webhook Payload** + ```json + { + "websiteId": "website-uuid", + "url": "https://example.com/changed-page", + "changeType": "content_update", + "contentHash": "new-content-hash", + "metadata": { + "source": "external-system", + "timestamp": "2024-01-01T00:00:00Z" + } + } + ``` + +## Monitoring and Troubleshooting + +### Check System Status +```bash +# Test the cron endpoint +curl -X POST "https://your-domain.com/api/cron/scan?mode=all" + +# Check database logs +SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10; +``` + +### Common Issues + +1. **Scans Not Running** + - Check cron job configuration + - Verify database connection + - Review subscription limits + +2. **Change Detection Not Working** + - Ensure subscription tier supports change detection + - Check webhook endpoint accessibility + - Verify content hash computation + +3. **Performance Issues** + - Monitor scan frequency + - Check database performance + - Review resource usage + +## Development + +### Adding New Metrics +1. Update `metric_definitions` table +2. Modify `LighthouseScanner` class +3. Update UI components + +### Customizing Scan Logic +1. Modify `performScan` method in `LighthouseScanner` +2. Update `runLighthouse` simulation +3. Adjust result processing + +### Extending Subscription Tiers +1. Update `getSubscriptionLimits` method +2. Modify database schema +3. Update UI components + +## Security Considerations + +- **Authentication**: Manual endpoints require user authentication +- **Rate Limiting**: Built-in subscription-based limits +- **Input Validation**: All webhook inputs are validated +- **Audit Logging**: All activities are logged for security + +## Performance Optimization + +- **Batch Processing**: Multiple websites processed efficiently +- **Error Recovery**: Failed scans don't affect the system +- **Resource Management**: Controlled resource usage +- **Caching**: Optimized database queries + +## Support + +For issues or questions: +1. Check the troubleshooting section +2. Review application logs +3. Verify database setup +4. Test endpoints manually +5. Check subscription configuration + +## Future Enhancements + +- **Real-time Notifications**: Push notifications for scan results +- **Advanced Analytics**: Detailed performance insights +- **Custom Metrics**: User-defined performance metrics +- **Integration APIs**: Third-party service integrations +- **Machine Learning**: Predictive performance analysis \ No newline at end of file diff --git a/website-monitoring-frontend/docs/cron-setup-guide.md b/website-monitoring-frontend/docs/cron-setup-guide.md new file mode 100644 index 0000000..7f919cf --- /dev/null +++ b/website-monitoring-frontend/docs/cron-setup-guide.md @@ -0,0 +1,260 @@ +# Cron Job Setup Guide for Automatic Lighthouse Scanning + +This guide will help you set up automated cron jobs to run the Lighthouse scanning system for your website monitoring application. + +## Overview + +The automatic scanning system includes: +- **Scheduled Scans**: Periodic scans based on user-configured schedules +- **Change Detection**: Automatic scans triggered when website content changes +- **Subscription Limits**: Respects user subscription tiers and rate limits + +## Prerequisites + +1. **Environment Variables**: Ensure your `.env` file has the required Supabase configuration: + ```env + NEXT_PUBLIC_SUPABASE_URL=your_supabase_url + NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key + SUPABASE_SERVICE_ROLE_KEY=your_service_role_key + ``` + +2. **Database Setup**: Make sure all required tables are created using the `setup-database.sql` script. + +3. **Deployed Application**: Your Next.js application should be deployed and accessible via HTTPS. + +## Cron Job Configuration + +### Option 1: Using Vercel Cron Jobs (Recommended) + +If you're deploying on Vercel, you can use their built-in cron job feature: + +1. **Create a `vercel.json` file** in your project root: + ```json + { + "crons": [ + { + "path": "/api/cron/scan?mode=all", + "schedule": "0 */6 * * *" + } + ] + } + ``` + +2. **Schedule Explanation**: + - `0 */6 * * *` = Every 6 hours + - `0 */4 * * *` = Every 4 hours + - `0 */2 * * *` = Every 2 hours + - `0 * * * *` = Every hour + - `*/15 * * * *` = Every 15 minutes + +3. **Deploy to Vercel**: The cron jobs will automatically start working after deployment. + +### Option 2: Using External Cron Services + +#### A. Cron-job.org (Free) + +1. Go to [cron-job.org](https://cron-job.org) +2. Create an account and add a new cron job +3. Set the URL to: `https://your-domain.com/api/cron/scan?mode=all` +4. Configure the schedule (recommended: every 6 hours) +5. Enable monitoring and notifications + +#### B. EasyCron (Free tier available) + +1. Go to [easycron.com](https://easycron.com) +2. Create an account and add a new cron job +3. Set the URL to: `https://your-domain.com/api/cron/scan?mode=all` +4. Configure the schedule +5. Set up email notifications for failures + +#### C. GitHub Actions (Free for public repos) + +1. Create `.github/workflows/cron-scan.yml`: + ```yaml + name: Lighthouse Scan Cron Job + + on: + schedule: + - cron: '0 */6 * * *' # Every 6 hours + + jobs: + scan: + runs-on: ubuntu-latest + steps: + - name: Trigger Scan + run: | + curl -X POST "https://your-domain.com/api/cron/scan?mode=all" + ``` + +### Option 3: Server Cron Jobs (VPS/Dedicated Server) + +If you're running on a VPS or dedicated server: + +1. **SSH into your server** +2. **Edit crontab**: `crontab -e` +3. **Add the cron job**: + ```bash + # Run every 6 hours + 0 */6 * * * curl -X POST "https://your-domain.com/api/cron/scan?mode=all" + + # Or run every hour + 0 * * * * curl -X POST "https://your-domain.com/api/cron/scan?mode=all" + ``` + +## API Endpoints + +The cron system provides several endpoints for different scan modes: + +### 1. Full Scan (Recommended for cron jobs) +``` +POST /api/cron/scan?mode=all +``` +- Runs both scheduled scans and change detection +- Respects subscription limits +- Returns scan statistics + +### 2. Scheduled Scans Only +``` +POST /api/cron/scan?mode=scheduled +``` +- Only runs scans based on user-configured schedules +- Useful for testing or specific use cases + +### 3. Change Detection Only +``` +POST /api/cron/scan?mode=change_detection +``` +- Only checks for website changes and triggers scans +- Can be run more frequently than full scans + +### 4. Manual Scan Trigger +``` +POST /api/cron/scan +``` +- Triggers a scan for a specific website +- Requires authentication +- Used by the ScanScheduleManager component + +## Monitoring and Logging + +### 1. Check Cron Job Status + +You can monitor if your cron jobs are working by: + +1. **Checking the API response**: + ```bash + curl -X POST "https://your-domain.com/api/cron/scan?mode=all" + ``` + +2. **Expected response**: + ```json + { + "success": true, + "message": "Scan processing completed", + "statistics": { + "scheduledScansProcessed": 5, + "changeDetectionChecks": 10, + "scansTriggered": 3, + "errors": 0 + } + } + ``` + +### 2. Database Logs + +Check the `audit_logs` table for scan activities: +```sql +SELECT * FROM audit_logs +WHERE action_type IN ('scan_scheduled', 'scan_triggered', 'change_detected') +ORDER BY created_at DESC +LIMIT 10; +``` + +### 3. Error Monitoring + +Set up monitoring for: +- HTTP 500 errors on the cron endpoint +- Database connection failures +- Subscription limit violations + +## Testing Your Setup + +### 1. Manual Test +```bash +# Test the cron endpoint manually +curl -X POST "https://your-domain.com/api/cron/scan?mode=all" +``` + +### 2. Check Database +```sql +-- Check if scans are being created +SELECT * FROM scans ORDER BY created_at DESC LIMIT 5; + +-- Check if scan results are being saved +SELECT * FROM scan_results ORDER BY created_at DESC LIMIT 5; +``` + +### 3. Monitor Logs +Check your application logs for any errors or warnings related to the scanning process. + +## Troubleshooting + +### Common Issues + +1. **Cron job not running**: + - Check if the URL is accessible + - Verify HTTPS is working + - Check server logs for errors + +2. **No scans being triggered**: + - Verify database tables exist + - Check subscription tier configuration + - Ensure websites have scan schedules configured + +3. **Rate limiting issues**: + - Check subscription limits in the database + - Verify the `subscription_limits` table has correct data + +4. **Authentication errors**: + - Verify `SUPABASE_SERVICE_ROLE_KEY` is set correctly + - Check if the service role has proper permissions + +### Debug Mode + +Enable debug logging by setting: +```env +TASKMASTER_LOG_LEVEL=debug +``` + +This will provide more detailed logs about the scanning process. + +## Security Considerations + +1. **API Protection**: Consider adding authentication to the cron endpoint if needed +2. **Rate Limiting**: The system already includes subscription-based rate limiting +3. **Error Handling**: Failed scans are logged and don't affect the overall system +4. **Data Privacy**: Only scan websites that users have explicitly added + +## Performance Optimization + +1. **Scan Frequency**: Start with every 6 hours, adjust based on usage +2. **Batch Processing**: The system processes multiple websites in batches +3. **Error Recovery**: Failed scans are retried automatically +4. **Resource Usage**: Monitor server resources during scan execution + +## Next Steps + +1. **Set up the cron job** using one of the methods above +2. **Test the system** with a few websites +3. **Monitor performance** and adjust scan frequency as needed +4. **Set up alerts** for cron job failures +5. **Configure webhooks** for external change detection triggers + +## Support + +If you encounter issues: +1. Check the troubleshooting section above +2. Review application logs +3. Verify database setup +4. Test the API endpoint manually +5. Check subscription configuration \ No newline at end of file diff --git a/website-monitoring-frontend/eslint.config.mjs b/website-monitoring-frontend/eslint.config.mjs new file mode 100644 index 0000000..3a3b2ac --- /dev/null +++ b/website-monitoring-frontend/eslint.config.mjs @@ -0,0 +1,22 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), + { + rules: { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + }, + }, +]; + +export default eslintConfig; diff --git a/website-monitoring-frontend/jest.config.ts b/website-monitoring-frontend/jest.config.ts new file mode 100644 index 0000000..9cd25b2 --- /dev/null +++ b/website-monitoring-frontend/jest.config.ts @@ -0,0 +1,15 @@ +import nextJest from 'next/jest.js'; + +const createJestConfig = nextJest({ + dir: './', +}); + +const customJestConfig = { + testEnvironment: 'jsdom', + moduleNameMapper: { + '^@/(.*)$': '<rootDir>/src/$1', + }, + setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], +}; + +export default createJestConfig(customJestConfig); diff --git a/website-monitoring-frontend/jest.setup.ts b/website-monitoring-frontend/jest.setup.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/website-monitoring-frontend/jest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/website-monitoring-frontend/next.config.ts b/website-monitoring-frontend/next.config.ts new file mode 100644 index 0000000..f0c7b13 --- /dev/null +++ b/website-monitoring-frontend/next.config.ts @@ -0,0 +1,10 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + eslint: { + // Do not fail production builds due to ESLint errors + ignoreDuringBuilds: true, + }, +}; + +export default nextConfig; diff --git a/website-monitoring-frontend/package-lock.json b/website-monitoring-frontend/package-lock.json new file mode 100644 index 0000000..88f67f0 --- /dev/null +++ b/website-monitoring-frontend/package-lock.json @@ -0,0 +1,14910 @@ +{ + "name": "website-monitoring", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "website-monitoring", + "version": "0.1.0", + "dependencies": { + "@hookform/resolvers": "^4.1.2", + "@radix-ui/react-checkbox": "^1.2.2", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-progress": "^1.1.3", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@shadcn/ui": "^0.0.4", + "@supabase/auth-helpers-nextjs": "^0.10.0", + "@supabase/ssr": "^0.5.2", + "@supabase/supabase-js": "^2.50.0", + "autoprefixer": "^10.4.20", + "chrome-launcher": "^1.2.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dotenv": "^16.5.0", + "framer-motion": "^12.4.10", + "jsdom": "^26.0.0", + "lighthouse": "^12.6.1", + "lucide-react": "^0.477.0", + "next": "^15.2.4", + "postcss": "^8.5.3", + "puppeteer": "^24.7.0", + "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", + "react-circular-progressbar": "^2.2.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-intersection-observer": "^9.16.0", + "recharts": "^2.15.1", + "supabase": "^2.15.8", + "tailwind-merge": "^3.0.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "concurrently": "^9.1.2", + "eslint": "^9", + "eslint-config-next": "15.2.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "tailwindcss": "^4.0.9", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "^5" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz", + "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.1", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz", + "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/icu-skeleton-parser": "1.8.14", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz", + "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", + "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz", + "integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", + "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz", + "integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", + "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", + "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", + "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", + "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", + "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", + "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", + "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", + "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz", + "integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz", + "integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", + "integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz", + "integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", + "integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz", + "integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz", + "integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz", + "integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz", + "integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz", + "integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT", + "peer": true + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", + "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@next/env": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.4.tgz", + "integrity": "sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.0.tgz", + "integrity": "sha512-jHFUG2OwmAuOASqq253RAEG/5BYcPHn27p1NoWZDCf4OdvdK0yRYWX92YKkL+Mk2s+GyJrmd/GATlL5b2IySpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.4.tgz", + "integrity": "sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.4.tgz", + "integrity": "sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.4.tgz", + "integrity": "sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.4.tgz", + "integrity": "sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.4.tgz", + "integrity": "sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.4.tgz", + "integrity": "sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.4.tgz", + "integrity": "sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.4.tgz", + "integrity": "sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@paulirish/trace_engine": { + "version": "0.0.53", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.53.tgz", + "integrity": "sha512-PUl/vlfo08Oj804VI5nDPeSk9vyslnBlVzDDwFt8SUVxY8+KdGMkra/vrXjEEHe8gb7+RqVTfOIlGw0nyrEelA==", + "license": "BSD-3-Clause", + "dependencies": { + "legacy-javascript": "latest", + "third-party-web": "latest" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", + "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.0.8", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", + "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", + "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", + "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.3.tgz", + "integrity": "sha512-Ausx+Jw1pAMbIBHStoQ6ZqDZR60PsCByvHdw/jdH9AqPrNE9xlBSf9EwcycvmrzwyKspSLaB52grlje2cRIUMg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.3.tgz", + "integrity": "sha512-vyy11fCGpkGK3qI5DSXOjgIboBZTriw0YDx/0KyX5CjIjDDNgp5AGgpgFkfZyiYiaU2Ww3iFuKo4wHmBusz1uA==", + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/integrations": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.3.tgz", + "integrity": "sha512-6i/lYp0BubHPDTg91/uxHvNui427df9r17SsIEXa2eKDwQ9gW2qRx5IWgvnxs2GV/GfSbwcx4swUB3RfEWrXrQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3", + "localforage": "^1.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.3.tgz", + "integrity": "sha512-t+QtekZedEfiZjbkRAk1QWJPnJlFBH/ti96tQhEq7wmlk3VszDXraZvLWZA0P2vXyglKzbWRGkT31aD3/kX+5Q==", + "license": "MIT", + "dependencies": { + "@sentry-internal/tracing": "7.120.3", + "@sentry/core": "7.120.3", + "@sentry/integrations": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/types": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.3.tgz", + "integrity": "sha512-C4z+3kGWNFJ303FC+FxAd4KkHvxpNFYAFN8iMIgBwJdpIl25KZ8Q/VdGn0MLLUEHNLvjob0+wvwlcRBBNLXOow==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.3.tgz", + "integrity": "sha512-UDAOQJtJDxZHQ5Nm1olycBIsz2wdGX8SdzyGVHmD8EOQYAeDZQyIlQYohDe9nazdIOQLZCIc3fU0G9gqVLkaGQ==", + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@shadcn/ui": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@shadcn/ui/-/ui-0.0.4.tgz", + "integrity": "sha512-0dtu/5ApsOZ24qgaZwtif8jVwqol7a4m1x5AxPuM1k5wxhqU7t/qEfBGtaSki1R8VlbTQfCj5PAlO45NKCa7Gg==", + "license": "MIT", + "dependencies": { + "chalk": "5.2.0", + "commander": "^10.0.0", + "execa": "^7.0.0", + "fs-extra": "^11.1.0", + "node-fetch": "^3.3.0", + "ora": "^6.1.2", + "prompts": "^2.4.2", + "zod": "^3.20.2" + }, + "bin": { + "ui": "dist/index.js" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@supabase/auth-helpers-nextjs": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-helpers-nextjs/-/auth-helpers-nextjs-0.10.0.tgz", + "integrity": "sha512-2dfOGsM4yZt0oS4TPiE7bD4vf7EVz7NRz/IJrV6vLg0GP7sMUx8wndv2euLGq4BjN9lUCpu6DG/uCC8j+ylwPg==", + "deprecated": "This package is now deprecated - please use the @supabase/ssr package instead.", + "license": "MIT", + "dependencies": { + "@supabase/auth-helpers-shared": "0.7.0", + "set-cookie-parser": "^2.6.0" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.39.8" + } + }, + "node_modules/@supabase/auth-helpers-shared": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-helpers-shared/-/auth-helpers-shared-0.7.0.tgz", + "integrity": "sha512-FBFf2ei2R7QC+B/5wWkthMha8Ca2bWHAndN+syfuEUUfufv4mLcAgBCcgNg5nJR8L0gZfyuaxgubtOc9aW3Cpg==", + "deprecated": "This package is now deprecated - please use the @supabase/ssr package instead.", + "license": "MIT", + "dependencies": { + "jose": "^4.14.4" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.39.8" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.70.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.70.0.tgz", + "integrity": "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", + "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.11.10", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.10.tgz", + "integrity": "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.13", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.5.2.tgz", + "integrity": "sha512-n3plRhr2Bs8Xun1o4S3k1CDv17iH5QY9YcoEvXX3bxV1/5XSasA0mNXYycFmADIdtdE6BG9MRjP5CGIs8qxC8A==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.7.0" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.43.4" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.0.tgz", + "integrity": "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.70.0", + "@supabase/functions-js": "2.4.4", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.19.4", + "@supabase/realtime-js": "2.11.10", + "@supabase/storage-js": "2.7.1" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz", + "integrity": "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.10" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz", + "integrity": "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.10", + "@tailwindcss/oxide-darwin-arm64": "4.1.10", + "@tailwindcss/oxide-darwin-x64": "4.1.10", + "@tailwindcss/oxide-freebsd-x64": "4.1.10", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.10", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.10", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.10", + "@tailwindcss/oxide-linux-x64-musl": "4.1.10", + "@tailwindcss/oxide-wasm32-wasi": "4.1.10", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.10", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.10" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.10.tgz", + "integrity": "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.10.tgz", + "integrity": "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.10.tgz", + "integrity": "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.10.tgz", + "integrity": "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.10.tgz", + "integrity": "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.10.tgz", + "integrity": "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.10.tgz", + "integrity": "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.10.tgz", + "integrity": "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.10.tgz", + "integrity": "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.10.tgz", + "integrity": "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.10", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz", + "integrity": "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.10.tgz", + "integrity": "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.10.tgz", + "integrity": "sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.10", + "@tailwindcss/oxide": "4.1.10", + "postcss": "^8.4.41", + "tailwindcss": "4.1.10" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz", + "integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz", + "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", + "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/type-utils": "8.34.1", + "@typescript-eslint/utils": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.34.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", + "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", + "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.34.1", + "@typescript-eslint/types": "^8.34.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", + "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", + "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", + "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.34.1", + "@typescript-eslint/utils": "8.34.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", + "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", + "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.34.1", + "@typescript-eslint/tsconfig-utils": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", + "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", + "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.34.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.1.tgz", + "integrity": "sha512-dd7yIp1hfJFX9ZlVLQRrh/Re9WMUHHmF9hrKD1yIvxcyNr2BhQ3xc1upAVhy8NijadnCswAxWQu8MkkSMC1qXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.1.tgz", + "integrity": "sha512-EzUPcMFtDVlo5yrbzMqUsGq3HnLXw+3ZOhSd7CUaDmbTtnrzM+RO2ntw2dm2wjbbc5djWj3yX0wzbbg8pLhx8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.1.tgz", + "integrity": "sha512-nB+dna3q4kOleKFcSZJ/wDXIsAd1kpMO9XrVAt8tG3RDWJ6vi+Ic6bpz4cmg5tWNeCfHEY4KuqJCB+pKejPEmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.1.tgz", + "integrity": "sha512-aKWHCrOGaCGwZcekf3TnczQoBxk5w//W3RZ4EQyhux6rKDwBPgDU9Y2yGigCV1Z+8DWqZgVGQi+hdpnlSy3a1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.1.tgz", + "integrity": "sha512-4dIEMXrXt0UqDVgrsUd1I+NoIzVQWXy/CNhgpfS75rOOMK/4Abn0Mx2M2gWH4Mk9+ds/ASAiCmqoUFynmMY5hA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.1.tgz", + "integrity": "sha512-vtvS13IXPs1eE8DuS/soiosqMBeyh50YLRZ+p7EaIKAPPeevRnA9G/wu/KbVt01ZD5qiGjxS+CGIdVC7I6gTOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.1.tgz", + "integrity": "sha512-BfdnN6aZ7NcX8djW8SR6GOJc+K+sFhWRF4vJueVE0vbUu5N1bLnBpxJg1TGlhSyo+ImC4SR0jcNiKN0jdoxt+A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.1.tgz", + "integrity": "sha512-Jhge7lFtH0QqfRz2PyJjJXWENqywPteITd+nOS0L6AhbZli+UmEyGBd2Sstt1c+l9C+j/YvKTl9wJo9PPmsFNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.1.tgz", + "integrity": "sha512-ofdK/ow+ZSbSU0pRoB7uBaiRHeaAOYQFU5Spp87LdcPL/P1RhbCTMSIYVb61XWzsVEmYKjHFtoIE0wxP6AFvrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.1.tgz", + "integrity": "sha512-eC8SXVn8de67HacqU7PoGdHA+9tGbqfEdD05AEFRAB81ejeQtNi5Fx7lPcxpLH79DW0BnMAHau3hi4RVkHfSCw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.1.tgz", + "integrity": "sha512-fIkwvAAQ41kfoGWfzeJ33iLGShl0JEDZHrMnwTHMErUcPkaaZRJYjQjsFhMl315NEQ4mmTlC+2nfK/J2IszDOw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.1.tgz", + "integrity": "sha512-RAAszxImSOFLk44aLwnSqpcOdce8sBcxASledSzuFAd8Q5ZhhVck472SisspnzHdc7THCvGXiUeZ2hOC7NUoBQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.1.tgz", + "integrity": "sha512-QoP9vkY+THuQdZi05bA6s6XwFd6HIz3qlx82v9bTOgxeqin/3C12Ye7f7EOD00RQ36OtOPWnhEMMm84sv7d1XQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.1.tgz", + "integrity": "sha512-/p77cGN/h9zbsfCseAP5gY7tK+7+DdM8fkPfr9d1ye1fsF6bmtGbtZN6e/8j4jCZ9NEIBBkT0GhdgixSelTK9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.1.tgz", + "integrity": "sha512-wInTqT3Bu9u50mDStEig1v8uxEL2Ht+K8pir/YhyyrM5ordJtxoqzsL1vR/CQzOJuDunUTrDkMM0apjW/d7/PA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.1.tgz", + "integrity": "sha512-eNwqO5kUa+1k7yFIircwwiniKWA0UFHo2Cfm8LYgkh9km7uMad+0x7X7oXbQonJXlqfitBTSjhA0un+DsHIrhw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.1.tgz", + "integrity": "sha512-Eaz1xMUnoa2mFqh20mPqSdbYl6crnk8HnIXDu6nsla9zpgZJZO8w3c1gvNN/4Eb0RXRq3K9OG6mu8vw14gIqiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.1.tgz", + "integrity": "sha512-H/+d+5BGlnEQif0gnwWmYbYv7HJj563PUKJfn8PlmzF8UmF+8KxdvXdwCsoOqh4HHnENnoLrav9NYBrv76x1wQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.1.tgz", + "integrity": "sha512-rS86wI4R6cknYM3is3grCb/laE8XBEbpWAMSIPjYfmYp75KL5dT87jXF2orDa4tQYg5aajP5G8Fgh34dRyR+Rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz", + "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bin-links": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-5.0.0.tgz", + "integrity": "sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==", + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/bin-links/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/bin-links/node_modules/write-file-atomic": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz", + "integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001724", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz", + "integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", + "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-launcher": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.0.tgz", + "integrity": "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.cjs" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-bidi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", + "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmd-shim": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-7.0.0.tgz", + "integrity": "sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/create-jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/csp_evaluator": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.5.tgz", + "integrity": "sha512-EL/iN9etCTzw/fBnp0/uj0f5BOOGvZut2mzsiiBZ/FdT6gFQCKRO/tmcKOxn5drWZ2Ndm/xBb1SI4zwWbGtmIw==", + "license": "Apache-2.0" + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.5.0.tgz", + "integrity": "sha512-/7gw8TGrvH/0g564EnhgFZogTMVe+lifpB7LWU+PEsiq5o83TUXR3fDbzTRXOJhoJwck5IS9ez3Em5LNMMO2aw==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1467305", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1467305.tgz", + "integrity": "sha512-LxwMLqBoPPGpMdRL4NkLFRNy3QLp6Uqa7GNp1v6JaBheop2QrB9Q7q0A/q/CYYP9sBfZdHOyszVx4gc9zyk7ow==", + "license": "BSD-3-Clause" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.171", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz", + "integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==", + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", + "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.1", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.29.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.0.tgz", + "integrity": "sha512-LkG0KKpinAoNPk2HXSx0fImFb/hQ6RnhSxTkpJFTkQ0SmnzsbRsjjN95WC/mDY34nKOenpptYEVvfkCR/h+VjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.2.0", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "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.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.18.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.18.1.tgz", + "integrity": "sha512-6o4EDuRPLk4LSZ1kRnnEOurbQ86MklVk+Y1rFBUKiF+d2pCdvMjWVu0ZkyMVCTwl5UyTH2n/zJEJx+jvTYuxow==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.18.1", + "motion-utils": "^12.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-link-header": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.3.tgz", + "integrity": "sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "license": "MIT" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/intl-messageformat": { + "version": "10.7.16", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", + "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.2", + "tslib": "^2.8.0" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/jest-changed-files/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-changed-files/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/js-library-detector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", + "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/legacy-javascript": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.1.tgz", + "integrity": "sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg==", + "license": "Apache-2.0" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lighthouse": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.6.1.tgz", + "integrity": "sha512-85WDkjcXAVdlFem9Y6SSxqoKiz/89UsDZhLpeLJIsJ4LlHxw047XTZhlFJmjYCB7K5S1erSBAf5cYLcfyNbH3A==", + "license": "Apache-2.0", + "dependencies": { + "@paulirish/trace_engine": "0.0.53", + "@sentry/node": "^7.0.0", + "axe-core": "^4.10.3", + "chrome-launcher": "^1.2.0", + "configstore": "^5.0.1", + "csp_evaluator": "1.1.5", + "devtools-protocol": "0.0.1467305", + "enquirer": "^2.3.6", + "http-link-header": "^1.1.1", + "intl-messageformat": "^10.5.3", + "jpeg-js": "^0.4.4", + "js-library-detector": "^6.7.0", + "lighthouse-logger": "^2.0.1", + "lighthouse-stack-packs": "1.12.2", + "lodash-es": "^4.17.21", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.3.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "puppeteer-core": "^24.10.0", + "robots-parser": "^3.0.1", + "semver": "^5.3.0", + "speedline-core": "^1.4.3", + "third-party-web": "^0.26.6", + "tldts-icann": "^6.1.16", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "chrome-debug": "core/scripts/manual-chrome-launcher.js", + "lighthouse": "cli/index.js", + "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" + }, + "engines": { + "node": ">=18.20" + } + }, + "node_modules/lighthouse-logger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", + "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/lighthouse-stack-packs": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.2.tgz", + "integrity": "sha512-Ug8feS/A+92TMTCK6yHYLwaFMuelK/hAKRMdldYkMNwv+d9PtWxjXEg6rwKtsUXTADajhdrhXyuNCJ5/sfmPFw==", + "license": "Apache-2.0" + }, + "node_modules/lighthouse/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/lighthouse/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.477.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.477.0.tgz", + "integrity": "sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/metaviewport-parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/motion-dom": { + "version": "12.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.18.1.tgz", + "integrity": "sha512-dR/4EYT23Snd+eUSLrde63Ws3oXQtJNw/krgautvTfwrN/2cHfCZMdu6CeTxVfRRWREW3Fy1f5vobRDiBb/q+w==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.18.1" + } + }, + "node_modules/motion-utils": { + "version": "12.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.18.1.tgz", + "integrity": "sha512-az26YDU4WoDP0ueAkUtABLk2BIxe28d8NH1qWT8jPGhPyf44XTdDUh8pDk9OPphaSrR9McgpcJlgwSOIw/sfkA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz", + "integrity": "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/next": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.3.4.tgz", + "integrity": "sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==", + "license": "MIT", + "dependencies": { + "@next/env": "15.3.4", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.3.4", + "@next/swc-darwin-x64": "15.3.4", + "@next/swc-linux-arm64-gnu": "15.3.4", + "@next/swc-linux-arm64-musl": "15.3.4", + "@next/swc-linux-x64-gnu": "15.3.4", + "@next/swc-linux-x64-musl": "15.3.4", + "@next/swc-win32-arm64-msvc": "15.3.4", + "@next/swc-win32-x64-msvc": "15.3.4", + "sharp": "^0.34.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz", + "integrity": "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.6.1", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.1.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "strip-ansi": "^7.0.1", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "24.10.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.10.2.tgz", + "integrity": "sha512-+k26rCz6akFZntx0hqUoFjCojgOLIxZs6p2k53LmEicwsT8F/FMBKfRfiBw1sitjiCvlR/15K7lBqfjXa251FA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1452169", + "puppeteer-core": "24.10.2", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.10.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.2.tgz", + "integrity": "sha512-CnzhOgrZj8DvkDqI+Yx+9or33i3Y9uUYbKyYpP4C13jWwXx/keQ38RMTMmxuLCWQlxjZrOH0Foq7P2fGP7adDQ==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1452169", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1452169", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz", + "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==", + "license": "BSD-3-Clause" + }, + "node_modules/puppeteer/node_modules/devtools-protocol": { + "version": "0.0.1452169", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz", + "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==", + "license": "BSD-3-Clause" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-circular-progressbar": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz", + "integrity": "sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==", + "license": "MIT", + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.58.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.58.1.tgz", + "integrity": "sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cmd-shim": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-5.0.0.tgz", + "integrity": "sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/recharts": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", + "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz", + "integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.2", + "@img/sharp-darwin-x64": "0.34.2", + "@img/sharp-libvips-darwin-arm64": "1.1.0", + "@img/sharp-libvips-darwin-x64": "1.1.0", + "@img/sharp-libvips-linux-arm": "1.1.0", + "@img/sharp-libvips-linux-arm64": "1.1.0", + "@img/sharp-libvips-linux-ppc64": "1.1.0", + "@img/sharp-libvips-linux-s390x": "1.1.0", + "@img/sharp-libvips-linux-x64": "1.1.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", + "@img/sharp-libvips-linuxmusl-x64": "1.1.0", + "@img/sharp-linux-arm": "0.34.2", + "@img/sharp-linux-arm64": "0.34.2", + "@img/sharp-linux-s390x": "0.34.2", + "@img/sharp-linux-x64": "0.34.2", + "@img/sharp-linuxmusl-arm64": "0.34.2", + "@img/sharp-linuxmusl-x64": "0.34.2", + "@img/sharp-wasm32": "0.34.2", + "@img/sharp-win32-arm64": "0.34.2", + "@img/sharp-win32-ia32": "0.34.2", + "@img/sharp-win32-x64": "0.34.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", + "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "license": "MIT", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supabase": { + "version": "2.26.9", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.26.9.tgz", + "integrity": "sha512-wHl7HtAD2iHMVXL8JZyfSjdI0WYM7EF0ydThp1tSvDANaD2JHCZc8GH1NdzglbwGqdHmjCYeSZ+H28fmucYl7Q==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bin-links": "^5.0.0", + "https-proxy-agent": "^7.0.2", + "node-fetch": "^3.3.2", + "tar": "7.4.3" + }, + "bin": { + "supabase": "bin/supabase" + }, + "engines": { + "npm": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", + "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==", + "license": "MIT" + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.10.tgz", + "integrity": "sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/third-party-web": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.26.7.tgz", + "integrity": "sha512-buUzX4sXC4efFX6xg2bw6/eZsCUh8qQwSavC4D9HpONMFlRbcHhD8Je5qwYdCpViR6q0qla2wPP+t91a2vgolg==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "node_modules/tldts-icann": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-6.1.86.tgz", + "integrity": "sha512-NFxmRT2lAEMcCOBgeZ0NuM0zsK/xgmNajnY6n4S1mwAKocft2s2ise1O3nQxrH3c+uY6hgHUV9GGNVp7tUE4Sg==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unrs-resolver": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.1.tgz", + "integrity": "sha512-4AZVxP05JGN6DwqIkSP4VKLOcwQa5l37SWHF/ahcuqBMbfxbpN1L1QKafEhWCziHhzKex9H/AR09H0OuVyU+9g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.2.2" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.9.1", + "@unrs/resolver-binding-android-arm64": "1.9.1", + "@unrs/resolver-binding-darwin-arm64": "1.9.1", + "@unrs/resolver-binding-darwin-x64": "1.9.1", + "@unrs/resolver-binding-freebsd-x64": "1.9.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.9.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.9.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.9.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.9.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.9.1", + "@unrs/resolver-binding-linux-x64-musl": "1.9.1", + "@unrs/resolver-binding-wasm32-wasi": "1.9.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.9.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.9.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.9.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/whatwg-url/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/website-monitoring-frontend/package.json b/website-monitoring-frontend/package.json new file mode 100644 index 0000000..868c754 --- /dev/null +++ b/website-monitoring-frontend/package.json @@ -0,0 +1,66 @@ +{ + "name": "website-monitoring", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "test": "jest --passWithNoTests", + "start": "next start", + "lint": "next lint", + "dev:all": "concurrently \"npm run dev\" \"cd ../backend && npm run dev\"" + }, + "dependencies": { + "@hookform/resolvers": "^4.1.2", + "@radix-ui/react-checkbox": "^1.2.2", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-progress": "^1.1.3", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@shadcn/ui": "^0.0.4", + "@supabase/auth-helpers-nextjs": "^0.10.0", + "@supabase/ssr": "^0.5.2", + "@supabase/supabase-js": "^2.50.0", + "autoprefixer": "^10.4.20", + "chrome-launcher": "^1.2.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dotenv": "^16.5.0", + "framer-motion": "^12.4.10", + "jsdom": "^26.0.0", + "lighthouse": "^12.6.1", + "lucide-react": "^0.477.0", + "next": "^15.2.4", + "postcss": "^8.5.3", + "puppeteer": "^24.7.0", + "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", + "react-circular-progressbar": "^2.2.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-intersection-observer": "^9.16.0", + "recharts": "^2.15.1", + "supabase": "^2.15.8", + "tailwind-merge": "^3.0.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "concurrently": "^9.1.2", + "eslint": "^9", + "eslint-config-next": "15.2.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "tailwindcss": "^4.0.9", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "^5" + } +} diff --git a/website-monitoring-frontend/pnpm-lock.yaml b/website-monitoring-frontend/pnpm-lock.yaml new file mode 100644 index 0000000..517149b --- /dev/null +++ b/website-monitoring-frontend/pnpm-lock.yaml @@ -0,0 +1,7304 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hookform/resolvers': + specifier: ^4.1.2 + version: 4.1.3(react-hook-form@7.62.0(react@19.1.1)) + '@radix-ui/react-checkbox': + specifier: ^1.2.2 + version: 1.3.2(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-label': + specifier: ^2.1.2 + version: 2.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-progress': + specifier: ^1.1.3 + version: 1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': + specifier: ^1.1.2 + version: 1.2.3(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-tabs': + specifier: ^1.1.3 + version: 1.1.12(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@shadcn/ui': + specifier: ^0.0.4 + version: 0.0.4 + '@supabase/auth-helpers-nextjs': + specifier: ^0.10.0 + version: 0.10.0(@supabase/supabase-js@2.54.0) + '@supabase/ssr': + specifier: ^0.5.2 + version: 0.5.2(@supabase/supabase-js@2.54.0) + '@supabase/supabase-js': + specifier: ^2.50.0 + version: 2.54.0 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.21(postcss@8.5.6) + chrome-launcher: + specifier: ^1.2.0 + version: 1.2.0 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + framer-motion: + specifier: ^12.4.10 + version: 12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + jsdom: + specifier: ^26.0.0 + version: 26.1.0 + lighthouse: + specifier: ^12.6.1 + version: 12.8.1 + lucide-react: + specifier: ^0.477.0 + version: 0.477.0(react@19.1.1) + next: + specifier: ^15.2.4 + version: 15.4.6(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + postcss: + specifier: ^8.5.3 + version: 8.5.6 + puppeteer: + specifier: ^24.7.0 + version: 24.16.1(typescript@5.9.2) + react: + specifier: ^19.0.0 + version: 19.1.1 + react-chartjs-2: + specifier: ^5.3.0 + version: 5.3.0(chart.js@4.5.0)(react@19.1.1) + react-circular-progressbar: + specifier: ^2.2.0 + version: 2.2.0(react@19.1.1) + react-dom: + specifier: ^19.0.0 + version: 19.1.1(react@19.1.1) + react-hook-form: + specifier: ^7.54.2 + version: 7.62.0(react@19.1.1) + react-intersection-observer: + specifier: ^9.16.0 + version: 9.16.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + recharts: + specifier: ^2.15.1 + version: 2.15.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + supabase: + specifier: ^2.15.8 + version: 2.33.9 + tailwind-merge: + specifier: ^3.0.2 + version: 3.3.1 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@4.1.11) + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.1 + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.11 + '@types/node': + specifier: ^20 + version: 20.19.10 + '@types/react': + specifier: ^19 + version: 19.1.9 + '@types/react-dom': + specifier: ^19 + version: 19.1.7(@types/react@19.1.9) + concurrently: + specifier: ^9.1.2 + version: 9.2.0 + eslint: + specifier: ^9 + version: 9.33.0(jiti@2.5.1) + eslint-config-next: + specifier: 15.2.0 + version: 15.2.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + tailwindcss: + specifier: ^4.0.9 + version: 4.1.11 + typescript: + specifier: ^5 + version: 5.9.2 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.28.2': + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@5.0.2': + resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.0.10': + resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.33.0': + resolution: {integrity: sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@formatjs/ecma402-abstract@2.3.4': + resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.2': + resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==} + + '@formatjs/icu-skeleton-parser@1.8.14': + resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==} + + '@formatjs/intl-localematcher@0.6.1': + resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==} + + '@hookform/resolvers@4.1.3': + resolution: {integrity: sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==} + peerDependencies: + react-hook-form: ^7.0.0 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@next/env@15.4.6': + resolution: {integrity: sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ==} + + '@next/eslint-plugin-next@15.2.0': + resolution: {integrity: sha512-jHFUG2OwmAuOASqq253RAEG/5BYcPHn27p1NoWZDCf4OdvdK0yRYWX92YKkL+Mk2s+GyJrmd/GATlL5b2IySpw==} + + '@next/swc-darwin-arm64@15.4.6': + resolution: {integrity: sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.4.6': + resolution: {integrity: sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.4.6': + resolution: {integrity: sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.4.6': + resolution: {integrity: sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.4.6': + resolution: {integrity: sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.4.6': + resolution: {integrity: sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.4.6': + resolution: {integrity: sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.4.6': + resolution: {integrity: sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation-amqplib@0.46.1': + resolution: {integrity: sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.43.1': + resolution: {integrity: sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dataloader@0.16.1': + resolution: {integrity: sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.47.1': + resolution: {integrity: sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.19.1': + resolution: {integrity: sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.43.1': + resolution: {integrity: sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.47.1': + resolution: {integrity: sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.45.2': + resolution: {integrity: sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.57.2': + resolution: {integrity: sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.47.1': + resolution: {integrity: sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.7.1': + resolution: {integrity: sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.44.1': + resolution: {integrity: sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.47.1': + resolution: {integrity: sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1': + resolution: {integrity: sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.52.0': + resolution: {integrity: sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.46.1': + resolution: {integrity: sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.45.2': + resolution: {integrity: sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.45.1': + resolution: {integrity: sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.51.1': + resolution: {integrity: sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis-4@0.46.1': + resolution: {integrity: sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.18.1': + resolution: {integrity: sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.10.1': + resolution: {integrity: sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/redis-common@0.36.2': + resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} + engines: {node: '>=14'} + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.36.0': + resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.40.1': + resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + + '@paulirish/trace_engine@0.0.57': + resolution: {integrity: sha512-s+JNJ53B1MiEqCOD2hnK96wFTRRStxFmtm6QaIe2jiNI+lkS9mYEgnHOH2caQ/dEPn6wY+f2u5dW6aFYjmbaiw==} + + '@prisma/instrumentation@6.11.1': + resolution: {integrity: sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==} + peerDependencies: + '@opentelemetry/api': ^1.8 + + '@puppeteer/browsers@2.10.6': + resolution: {integrity: sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==} + engines: {node: '>=18'} + hasBin: true + + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + + '@radix-ui/react-checkbox@1.3.2': + resolution: {integrity: sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.10': + resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.1.12': + resolution: {integrity: sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.12.0': + resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==} + + '@sentry/core@9.45.0': + resolution: {integrity: sha512-yTpB53fBEWTMzltD/8f/qI2MFTwgd2vSkn7pOZQusSOMtyt0Bsm/77oqXldIt+eMBAImZalzZaxmaN7RyiRKWQ==} + engines: {node: '>=18'} + + '@sentry/node-core@9.45.0': + resolution: {integrity: sha512-tzt60LO7P1m+0OLEqtL5Fd71PwKpg7dSOn3rqB7T6AJeDDiHsXV/yhUZiye1EWHTi0/yOcb0M1Ncjs8Cdyz9Nw==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.0.0 + '@opentelemetry/core': ^1.30.1 || ^2.0.0 + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/resources': ^1.30.1 || ^2.0.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0 + '@opentelemetry/semantic-conventions': ^1.34.0 + + '@sentry/node@9.45.0': + resolution: {integrity: sha512-c0SFcMeZwxLvjC1HrutI8V+Ag8AxENXPiU5PbSmqiTX7p4QnByTcxkENGw5EyLedDZluuEDmmHTBKckCC4X2nA==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@9.45.0': + resolution: {integrity: sha512-xLH7ZH6xcZBHK77mTa32YjIEL92jmc7i2qkxlchzTNacmTn9BNnuzPFBS7KuISJPXw9R1pXBra6IVEhm6hil/g==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.0.0 + '@opentelemetry/core': ^1.30.1 || ^2.0.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0 + '@opentelemetry/semantic-conventions': ^1.34.0 + + '@shadcn/ui@0.0.4': + resolution: {integrity: sha512-0dtu/5ApsOZ24qgaZwtif8jVwqol7a4m1x5AxPuM1k5wxhqU7t/qEfBGtaSki1R8VlbTQfCj5PAlO45NKCa7Gg==} + hasBin: true + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@supabase/auth-helpers-nextjs@0.10.0': + resolution: {integrity: sha512-2dfOGsM4yZt0oS4TPiE7bD4vf7EVz7NRz/IJrV6vLg0GP7sMUx8wndv2euLGq4BjN9lUCpu6DG/uCC8j+ylwPg==} + deprecated: This package is now deprecated - please use the @supabase/ssr package instead. + peerDependencies: + '@supabase/supabase-js': ^2.39.8 + + '@supabase/auth-helpers-shared@0.7.0': + resolution: {integrity: sha512-FBFf2ei2R7QC+B/5wWkthMha8Ca2bWHAndN+syfuEUUfufv4mLcAgBCcgNg5nJR8L0gZfyuaxgubtOc9aW3Cpg==} + deprecated: This package is now deprecated - please use the @supabase/ssr package instead. + peerDependencies: + '@supabase/supabase-js': ^2.39.8 + + '@supabase/auth-js@2.71.1': + resolution: {integrity: sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==} + + '@supabase/functions-js@2.4.5': + resolution: {integrity: sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==} + + '@supabase/node-fetch@2.6.15': + resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==} + engines: {node: 4.x || >=6.0.0} + + '@supabase/postgrest-js@1.19.4': + resolution: {integrity: sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==} + + '@supabase/realtime-js@2.15.0': + resolution: {integrity: sha512-SEIWApsxyoAe68WU2/5PCCuBwa11LL4Bb8K3r2FHCt3ROpaTthmDiWEhnLMGayP05N4QeYrMk0kyTZOwid/Hjw==} + + '@supabase/ssr@0.5.2': + resolution: {integrity: sha512-n3plRhr2Bs8Xun1o4S3k1CDv17iH5QY9YcoEvXX3bxV1/5XSasA0mNXYycFmADIdtdE6BG9MRjP5CGIs8qxC8A==} + peerDependencies: + '@supabase/supabase-js': ^2.43.4 + + '@supabase/storage-js@2.10.4': + resolution: {integrity: sha512-cvL02GarJVFcNoWe36VBybQqTVRq6wQSOCvTS64C+eyuxOruFIm1utZAY0xi2qKtHJO3EjKaj8iWJKySusDmAQ==} + + '@supabase/supabase-js@2.54.0': + resolution: {integrity: sha512-DLw83YwBfAaFiL3oWV26+sHRdeCGtxmIKccjh/Pndze3BWM4fZghzYKhk3ElOQU8Bluq4AkkCJ5bM5Szl/sfRg==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.11': + resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} + + '@tailwindcss/oxide-android-arm64@4.1.11': + resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.11': + resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.11': + resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.11': + resolution: {integrity: sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/mysql@2.15.26': + resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} + + '@types/node@20.19.10': + resolution: {integrity: sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==} + + '@types/pg-pool@2.0.6': + resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + + '@types/pg@8.6.1': + resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + + '@types/phoenix@1.6.6': + resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} + + '@types/react-dom@19.1.7': + resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.9': + resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==} + + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@typescript-eslint/eslint-plugin@8.39.0': + resolution: {integrity: sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.39.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.39.0': + resolution: {integrity: sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.39.0': + resolution: {integrity: sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.39.0': + resolution: {integrity: sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.39.0': + resolution: {integrity: sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.39.0': + resolution: {integrity: sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.39.0': + resolution: {integrity: sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.39.0': + resolution: {integrity: sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.39.0': + resolution: {integrity: sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.39.0': + resolution: {integrity: sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + atomically@2.0.3: + resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-events@2.6.1: + resolution: {integrity: sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==} + + bare-fs@4.1.6: + resolution: {integrity: sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.6.1: + resolution: {integrity: sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.6.5: + resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + + bin-links@5.0.0: + resolution: {integrity: sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==} + engines: {node: ^18.17.0 || >=20.5.0} + + bl@5.1.0: + resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.2: + resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001734: + resolution: {integrity: sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.2.0: + resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chart.js@4.5.0: + resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} + engines: {pnpm: '>=8'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chrome-launcher@1.2.0: + resolution: {integrity: sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==} + engines: {node: '>=12.13.0'} + hasBin: true + + chromium-bidi@7.3.1: + resolution: {integrity: sha512-i+BMGluhZZc4Jic9L1aHJBTfaopxmCqQxGklyMcqFx4fvF3nI4BJ3bCe1ad474nvYRIo/ZN/VrdA4eOaRZua4Q==} + peerDependencies: + devtools-protocol: '*' + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmd-shim@7.0.0: + resolution: {integrity: sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==} + engines: {node: ^18.17.0 || >=20.5.0} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concurrently@9.2.0: + resolution: {integrity: sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==} + engines: {node: '>=18'} + hasBin: true + + configstore@7.0.0: + resolution: {integrity: sha512-yk7/5PN5im4qwz0WFZW3PXnzHgPu9mX29Y8uZ3aefe2lBPC1FYttWZRcaW9fKkT0pBCJyuQ2HfbmPVaODi9jcQ==} + engines: {node: '>=18'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csp_evaluator@1.1.5: + resolution: {integrity: sha512-EL/iN9etCTzw/fBnp0/uj0f5BOOGvZut2mzsiiBZ/FdT6gFQCKRO/tmcKOxn5drWZ2Ndm/xBb1SI4zwWbGtmIw==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + devtools-protocol@0.0.1475386: + resolution: {integrity: sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==} + + devtools-protocol@0.0.1478340: + resolution: {integrity: sha512-EqhRVWo+j3O1a5LEvZi5fFlBRhvciqYoCHpsEfPcIpA/Abh0W1LF+V3AIvQD9Z4Apj0+p3U07vb7uXfn2hm3HQ==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dot-prop@9.0.0: + resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} + engines: {node: '>=18'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.199: + resolution: {integrity: sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-config-next@15.2.0: + resolution: {integrity: sha512-LkG0KKpinAoNPk2HXSx0fImFb/hQ6RnhSxTkpJFTkQ0SmnzsbRsjjN95WC/mDY34nKOenpptYEVvfkCR/h+VjA==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.33.0: + resolution: {integrity: sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.2.2: + resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} + engines: {node: '>=6.0.0'} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + framer-motion@12.23.12: + resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} + engines: {node: '>=14.14'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-link-header@1.1.3: + resolution: {integrity: sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==} + engines: {node: '>=6.0.0'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + image-ssim@0.2.0: + resolution: {integrity: sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-in-the-middle@1.14.2: + resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + intl-messageformat@10.7.16: + resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + hasBin: true + + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + + js-library-detector@6.7.0: + resolution: {integrity: sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==} + engines: {node: '>=12'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + legacy-javascript@0.0.1: + resolution: {integrity: sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lighthouse-logger@2.0.2: + resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} + + lighthouse-stack-packs@1.12.2: + resolution: {integrity: sha512-Ug8feS/A+92TMTCK6yHYLwaFMuelK/hAKRMdldYkMNwv+d9PtWxjXEg6rwKtsUXTADajhdrhXyuNCJ5/sfmPFw==} + + lighthouse@12.8.1: + resolution: {integrity: sha512-z0ceMjEM16C0HuADzMFgXe32BGJ7aJMSx48qTCcQYYjMUPxF1XWTP3rGXJEgB6P/VvS+yfp3TOMR3FpmLo0W3w==} + engines: {node: '>=18.16'} + hasBin: true + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@5.1.0: + resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} + engines: {node: '>=12'} + + lookup-closest-locale@6.2.0: + resolution: {integrity: sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lucide-react@0.477.0: + resolution: {integrity: sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + metaviewport-parser@0.3.0: + resolution: {integrity: sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + motion-dom@12.23.12: + resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==} + + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.3: + resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + next@15.4.6: + resolution: {integrity: sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm-normalize-package-bin@4.0.0: + resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nwsapi@2.2.21: + resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@6.3.1: + resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-cache-control@1.0.1: + resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + proc-log@5.0.0: + resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + puppeteer-core@24.16.1: + resolution: {integrity: sha512-0dGD2kxoH9jqj/xiz4KZLcPKpqWygs+VSEBzvuVbU3KoT2cCw4HnMT9r/7NvYl1lIa+JCa5yIyRqi+4R3UyYfQ==} + engines: {node: '>=18'} + + puppeteer@24.16.1: + resolution: {integrity: sha512-3jrx2BrOBb8yr3+KE7OyxVtI2fjPNZi46/SQGxFvlKZX4/56i2LbdArEhNvlQw/xxmsZfpjFRbGtkMavgh3I+g==} + engines: {node: '>=18'} + hasBin: true + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-chartjs-2@5.3.0: + resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==} + peerDependencies: + chart.js: ^4.1.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-circular-progressbar@2.2.0: + resolution: {integrity: sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==} + peerDependencies: + react: '>=0.14.0' + + react-dom@19.1.1: + resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + peerDependencies: + react: ^19.1.1 + + react-hook-form@7.62.0: + resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-intersection-observer@9.16.0: + resolution: {integrity: sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react-dom: + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.1.1: + resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + engines: {node: '>=0.10.0'} + + read-cmd-shim@5.0.0: + resolution: {integrity: sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==} + engines: {node: ^18.17.0 || >=20.5.0} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + robots-parser@3.0.1: + resolution: {integrity: sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==} + engines: {node: '>=10.0.0'} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.6: + resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + speedline-core@1.4.3: + resolution: {integrity: sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==} + engines: {node: '>=8.0'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stdin-discarder@0.1.0: + resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + streamx@2.22.1: + resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + stubborn-fs@1.2.5: + resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supabase@2.33.9: + resolution: {integrity: sha512-bjCdzcAzbzmPn5B4FNjsAE32aHDgCHtHngj0eDZdZ1+tVbH1/4TwGeZWy41JeiraNx5VPMG+BUOG2VNBXXcXEA==} + engines: {npm: '>=8'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@4.1.11: + resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar-fs@3.1.0: + resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + third-party-web@0.27.0: + resolution: {integrity: sha512-h0JYX+dO2Zr3abCQpS6/uFjujaOjA1DyDzGQ41+oFn9VW/ARiq9g5ln7qEP9+BTzDpOMyIfsfj4OvfgXAsMUSA==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts-icann@6.1.86: + resolution: {integrity: sha512-NFxmRT2lAEMcCOBgeZ0NuM0zsK/xgmNajnY6n4S1mwAKocft2s2ise1O3nQxrH3c+uY6hgHUV9GGNVp7tUE4Sg==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typed-query-selector@2.12.0: + resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + when-exit@2.1.4: + resolution: {integrity: sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@6.0.0: + resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/runtime@7.28.2': {} + + '@csstools/color-helpers@5.0.2': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.0.2 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@emnapi/core@1.4.5': + dependencies: + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.33.0(jiti@2.5.1))': + dependencies: + eslint: 9.33.0(jiti@2.5.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.33.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@formatjs/ecma402-abstract@2.3.4': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.1 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.2': + dependencies: + '@formatjs/ecma402-abstract': 2.3.4 + '@formatjs/icu-skeleton-parser': 1.8.14 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.14': + dependencies: + '@formatjs/ecma402-abstract': 2.3.4 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.1': + dependencies: + tslib: 2.8.1 + + '@hookform/resolvers@4.1.3(react-hook-form@7.62.0(react@19.1.1))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.62.0(react@19.1.1) + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/sharp-darwin-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.0 + optional: true + + '@img/sharp-darwin-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + optional: true + + '@img/sharp-linux-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.0 + optional: true + + '@img/sharp-linux-arm@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.0 + optional: true + + '@img/sharp-linux-ppc64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.0 + optional: true + + '@img/sharp-linux-s390x@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.0 + optional: true + + '@img/sharp-linux-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + optional: true + + '@img/sharp-wasm32@0.34.3': + dependencies: + '@emnapi/runtime': 1.4.5 + optional: true + + '@img/sharp-win32-arm64@0.34.3': + optional: true + + '@img/sharp-win32-ia32@0.34.3': + optional: true + + '@img/sharp-win32-x64@0.34.3': + optional: true + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@kurkle/color@0.3.4': {} + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@tybys/wasm-util': 0.10.0 + optional: true + + '@next/env@15.4.6': {} + + '@next/eslint-plugin-next@15.2.0': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@15.4.6': + optional: true + + '@next/swc-darwin-x64@15.4.6': + optional: true + + '@next/swc-linux-arm64-gnu@15.4.6': + optional: true + + '@next/swc-linux-arm64-musl@15.4.6': + optional: true + + '@next/swc-linux-x64-gnu@15.4.6': + optional: true + + '@next/swc-linux-x64-musl@15.4.6': + optional: true + + '@next/swc-win32-arm64-msvc@15.4.6': + optional: true + + '@next/swc-win32-x64-msvc@15.4.6': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.16.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.19.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.45.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + forwarded-parse: 2.1.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.52.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.45.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/mysql': 2.15.26 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.51.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + '@types/pg': 8.6.1 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis-4@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.18.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.10.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.14.2 + require-in-the-middle: 7.5.2 + semver: 7.7.2 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/redis-common@0.36.2': {} + + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/semantic-conventions@1.28.0': {} + + '@opentelemetry/semantic-conventions@1.36.0': {} + + '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@paulirish/trace_engine@0.0.57': + dependencies: + legacy-javascript: 0.0.1 + third-party-web: 0.27.0 + + '@prisma/instrumentation@6.11.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@puppeteer/browsers@2.10.6': + dependencies: + debug: 4.4.1 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.5.0 + semver: 7.7.2 + tar-fs: 3.1.0 + yargs: 17.7.2 + transitivePeerDependencies: + - bare-buffer + - supports-color + + '@radix-ui/primitive@1.1.2': {} + + '@radix-ui/react-checkbox@1.3.2(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.9)(react@19.1.1)': + dependencies: + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-context@1.1.2(@types/react@19.1.9)(react@19.1.1)': + dependencies: + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-direction@1.1.1(@types/react@19.1.9)(react@19.1.1)': + dependencies: + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-id@1.1.1(@types/react@19.1.9)(react@19.1.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + + '@radix-ui/react-slot@1.2.3(@types/react@19.1.9)(react@19.1.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.9)(react@19.1.1)': + dependencies: + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.9)(react@19.1.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.9)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.9)(react@19.1.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.9)(react@19.1.1)': + dependencies: + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.9)(react@19.1.1)': + dependencies: + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.9)(react@19.1.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.1) + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.9 + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.12.0': {} + + '@sentry/core@9.45.0': {} + + '@sentry/node-core@9.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@sentry/core': 9.45.0 + '@sentry/opentelemetry': 9.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) + import-in-the-middle: 1.14.2 + + '@sentry/node@9.45.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.16.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.19.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.51.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.18.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.10.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@prisma/instrumentation': 6.11.1(@opentelemetry/api@1.9.0) + '@sentry/core': 9.45.0 + '@sentry/node-core': 9.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) + '@sentry/opentelemetry': 9.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) + import-in-the-middle: 1.14.2 + minimatch: 9.0.5 + transitivePeerDependencies: + - supports-color + + '@sentry/opentelemetry@9.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@sentry/core': 9.45.0 + + '@shadcn/ui@0.0.4': + dependencies: + chalk: 5.2.0 + commander: 10.0.1 + execa: 7.2.0 + fs-extra: 11.3.1 + node-fetch: 3.3.2 + ora: 6.3.1 + prompts: 2.4.2 + zod: 3.25.76 + + '@standard-schema/utils@0.3.0': {} + + '@supabase/auth-helpers-nextjs@0.10.0(@supabase/supabase-js@2.54.0)': + dependencies: + '@supabase/auth-helpers-shared': 0.7.0(@supabase/supabase-js@2.54.0) + '@supabase/supabase-js': 2.54.0 + set-cookie-parser: 2.7.1 + + '@supabase/auth-helpers-shared@0.7.0(@supabase/supabase-js@2.54.0)': + dependencies: + '@supabase/supabase-js': 2.54.0 + jose: 4.15.9 + + '@supabase/auth-js@2.71.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/functions-js@2.4.5': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/node-fetch@2.6.15': + dependencies: + whatwg-url: 5.0.0 + + '@supabase/postgrest-js@1.19.4': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/realtime-js@2.15.0': + dependencies: + '@supabase/node-fetch': 2.6.15 + '@types/phoenix': 1.6.6 + '@types/ws': 8.18.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/ssr@0.5.2(@supabase/supabase-js@2.54.0)': + dependencies: + '@supabase/supabase-js': 2.54.0 + '@types/cookie': 0.6.0 + cookie: 0.7.2 + + '@supabase/storage-js@2.10.4': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/supabase-js@2.54.0': + dependencies: + '@supabase/auth-js': 2.71.1 + '@supabase/functions-js': 2.4.5 + '@supabase/node-fetch': 2.6.15 + '@supabase/postgrest-js': 1.19.4 + '@supabase/realtime-js': 2.15.0 + '@supabase/storage-js': 2.10.4 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.11': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.11 + + '@tailwindcss/oxide-android-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide@4.1.11': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-x64': 4.1.11 + '@tailwindcss/oxide-freebsd-x64': 4.1.11 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-x64-musl': 4.1.11 + '@tailwindcss/oxide-wasm32-wasi': 4.1.11 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 + + '@tailwindcss/postcss@4.1.11': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.11 + '@tailwindcss/oxide': 4.1.11 + postcss: 8.5.6 + tailwindcss: 4.1.11 + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@tybys/wasm-util@0.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.10 + + '@types/cookie@0.6.0': {} + + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/mysql@2.15.26': + dependencies: + '@types/node': 20.19.10 + + '@types/node@20.19.10': + dependencies: + undici-types: 6.21.0 + + '@types/pg-pool@2.0.6': + dependencies: + '@types/pg': 8.6.1 + + '@types/pg@8.6.1': + dependencies: + '@types/node': 20.19.10 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + + '@types/phoenix@1.6.6': {} + + '@types/react-dom@19.1.7(@types/react@19.1.9)': + dependencies: + '@types/react': 19.1.9 + + '@types/react@19.1.9': + dependencies: + csstype: 3.1.3 + + '@types/shimmer@1.2.0': {} + + '@types/tedious@4.0.14': + dependencies: + '@types/node': 20.19.10 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.10 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 20.19.10 + optional: true + + '@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.39.0 + '@typescript-eslint/type-utils': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.39.0 + eslint: 9.33.0(jiti@2.5.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.39.0 + '@typescript-eslint/types': 8.39.0 + '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.39.0 + debug: 4.4.1 + eslint: 9.33.0(jiti@2.5.1) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.39.0(typescript@5.9.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.39.0(typescript@5.9.2) + '@typescript-eslint/types': 8.39.0 + debug: 4.4.1 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.39.0': + dependencies: + '@typescript-eslint/types': 8.39.0 + '@typescript-eslint/visitor-keys': 8.39.0 + + '@typescript-eslint/tsconfig-utils@8.39.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@typescript-eslint/type-utils@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + dependencies: + '@typescript-eslint/types': 8.39.0 + '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + debug: 4.4.1 + eslint: 9.33.0(jiti@2.5.1) + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.39.0': {} + + '@typescript-eslint/typescript-estree@8.39.0(typescript@5.9.2)': + dependencies: + '@typescript-eslint/project-service': 8.39.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.39.0(typescript@5.9.2) + '@typescript-eslint/types': 8.39.0 + '@typescript-eslint/visitor-keys': 8.39.0 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) + '@typescript-eslint/scope-manager': 8.39.0 + '@typescript-eslint/types': 8.39.0 + '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.9.2) + eslint: 9.33.0(jiti@2.5.1) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.39.0': + dependencies: + '@typescript-eslint/types': 8.39.0 + eslint-visitor-keys: 4.2.1 + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@7.1.4: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + async-function@1.0.0: {} + + atomically@2.0.3: + dependencies: + stubborn-fs: 1.2.5 + when-exit: 2.1.4 + + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.25.2 + caniuse-lite: 1.0.30001734 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.10.3: {} + + axobject-query@4.1.0: {} + + b4a@1.6.7: {} + + balanced-match@1.0.2: {} + + bare-events@2.6.1: + optional: true + + bare-fs@4.1.6: + dependencies: + bare-events: 2.6.1 + bare-path: 3.0.0 + bare-stream: 2.6.5(bare-events@2.6.1) + optional: true + + bare-os@3.6.1: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.1 + optional: true + + bare-stream@2.6.5(bare-events@2.6.1): + dependencies: + streamx: 2.22.1 + optionalDependencies: + bare-events: 2.6.1 + optional: true + + base64-js@1.5.1: {} + + basic-ftp@5.0.5: {} + + bin-links@5.0.0: + dependencies: + cmd-shim: 7.0.0 + npm-normalize-package-bin: 4.0.0 + proc-log: 5.0.0 + read-cmd-shim: 5.0.0 + write-file-atomic: 6.0.0 + + bl@5.1.0: + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 3.6.2 + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.2: + dependencies: + caniuse-lite: 1.0.30001734 + electron-to-chromium: 1.5.199 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.2) + + buffer-crc32@0.2.13: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001734: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.2.0: {} + + chart.js@4.5.0: + dependencies: + '@kurkle/color': 0.3.4 + + chownr@3.0.0: {} + + chrome-launcher@1.2.0: + dependencies: + '@types/node': 20.19.10 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 2.0.2 + transitivePeerDependencies: + - supports-color + + chromium-bidi@7.3.1(devtools-protocol@0.0.1475386): + dependencies: + devtools-protocol: 0.0.1475386 + mitt: 3.0.1 + zod: 3.25.76 + + cjs-module-lexer@1.4.3: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + + cli-spinners@2.9.2: {} + + client-only@0.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + clsx@2.1.1: {} + + cmd-shim@7.0.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + commander@10.0.1: {} + + concat-map@0.0.1: {} + + concurrently@9.2.0: + dependencies: + chalk: 4.1.2 + lodash: 4.17.21 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + configstore@7.0.0: + dependencies: + atomically: 2.0.3 + dot-prop: 9.0.0 + graceful-fs: 4.2.11 + xdg-basedir: 5.1.0 + + cookie@0.7.2: {} + + cosmiconfig@9.0.0(typescript@5.9.2): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csp_evaluator@1.1.5: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.1.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + damerau-levenshtein@1.0.8: {} + + data-uri-to-buffer@4.0.1: {} + + data-uri-to-buffer@6.0.2: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + decimal.js-light@2.5.1: {} + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + detect-libc@2.0.4: {} + + devtools-protocol@0.0.1475386: {} + + devtools-protocol@0.0.1478340: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.2 + csstype: 3.1.3 + + dot-prop@9.0.0: + dependencies: + type-fest: 4.41.0 + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.199: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + entities@6.0.1: {} + + env-paths@2.2.1: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-config-next@15.2.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2): + dependencies: + '@next/eslint-plugin-next': 15.2.0 + '@rushstack/eslint-patch': 1.12.0 + '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + eslint: 9.33.0(jiti@2.5.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.33.0(jiti@2.5.1)) + eslint-plugin-react: 7.37.5(eslint@9.33.0(jiti@2.5.1)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.33.0(jiti@2.5.1)) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.1 + eslint: 9.33.0(jiti@2.5.1) + get-tsconfig: 4.10.1 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.14 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + eslint: 9.33.0(jiti@2.5.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.33.0(jiti@2.5.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.39.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.33.0(jiti@2.5.1)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.10.3 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.33.0(jiti@2.5.1) + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.2.0(eslint@9.33.0(jiti@2.5.1)): + dependencies: + eslint: 9.33.0(jiti@2.5.1) + + eslint-plugin-react@7.37.5(eslint@9.33.0(jiti@2.5.1)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.33.0(jiti@2.5.1) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.33.0(jiti@2.5.1): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.33.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.5.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + eventemitter3@4.0.7: {} + + execa@7.2.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + + extract-zip@2.0.1: + dependencies: + debug: 4.4.1 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-equals@5.2.2: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded-parse@2.1.2: {} + + fraction.js@4.3.7: {} + + framer-motion@12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + motion-dom: 12.23.12 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + + fs-extra@11.3.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + get-caller-file@2.0.5: {} + + 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 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + get-stream@6.0.1: {} + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-link-header@1.1.3: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + human-signals@4.3.1: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + image-ssim@0.2.0: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-in-the-middle@1.14.2: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + internmap@2.0.3: {} + + intl-messageformat@10.7.16: + dependencies: + '@formatjs/ecma402-abstract': 2.3.4 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.2 + tslib: 2.8.1 + + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: + optional: true + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@2.0.0: {} + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@3.0.0: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-unicode-supported@1.3.0: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.5.1: {} + + jose@4.15.9: {} + + jpeg-js@0.4.4: {} + + js-library-detector@6.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsbn@1.1.0: {} + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.21 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + legacy-javascript@0.0.1: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lighthouse-logger@2.0.2: + dependencies: + debug: 4.4.1 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + + lighthouse-stack-packs@1.12.2: {} + + lighthouse@12.8.1: + dependencies: + '@paulirish/trace_engine': 0.0.57 + '@sentry/node': 9.45.0 + axe-core: 4.10.3 + chrome-launcher: 1.2.0 + configstore: 7.0.0 + csp_evaluator: 1.1.5 + devtools-protocol: 0.0.1478340 + enquirer: 2.4.1 + http-link-header: 1.1.3 + intl-messageformat: 10.7.16 + jpeg-js: 0.4.4 + js-library-detector: 6.7.0 + lighthouse-logger: 2.0.2 + lighthouse-stack-packs: 1.12.2 + lodash-es: 4.17.21 + lookup-closest-locale: 6.2.0 + metaviewport-parser: 0.3.0 + open: 8.4.2 + parse-cache-control: 1.0.1 + puppeteer-core: 24.16.1 + robots-parser: 3.0.1 + semver: 5.7.2 + speedline-core: 1.4.3 + third-party-web: 0.27.0 + tldts-icann: 6.1.86 + ws: 7.5.10 + yargs: 17.7.2 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - bare-buffer + - bufferutil + - supports-color + - utf-8-validate + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + log-symbols@5.1.0: + dependencies: + chalk: 5.2.0 + is-unicode-supported: 1.3.0 + + lookup-closest-locale@6.2.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + lru-cache@7.18.3: {} + + lucide-react@0.477.0(react@19.1.1): + dependencies: + react: 19.1.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + marky@1.3.0: {} + + math-intrinsics@1.1.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + metaviewport-parser@0.3.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mitt@3.0.1: {} + + mkdirp@3.0.1: {} + + module-details-from-path@1.0.4: {} + + motion-dom@12.23.12: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.3.3: {} + + natural-compare@1.4.0: {} + + netmask@2.0.2: {} + + next@15.4.6(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + '@next/env': 15.4.6 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001734 + postcss: 8.4.31 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + styled-jsx: 5.1.6(react@19.1.1) + optionalDependencies: + '@next/swc-darwin-arm64': 15.4.6 + '@next/swc-darwin-x64': 15.4.6 + '@next/swc-linux-arm64-gnu': 15.4.6 + '@next/swc-linux-arm64-musl': 15.4.6 + '@next/swc-linux-x64-gnu': 15.4.6 + '@next/swc-linux-x64-musl': 15.4.6 + '@next/swc-win32-arm64-msvc': 15.4.6 + '@next/swc-win32-x64-msvc': 15.4.6 + '@opentelemetry/api': 1.9.0 + sharp: 0.34.3 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.19: {} + + normalize-range@0.1.2: {} + + npm-normalize-package-bin@4.0.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + nwsapi@2.2.21: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@6.3.1: + dependencies: + chalk: 5.2.0 + cli-cursor: 4.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 1.3.0 + log-symbols: 5.1.0 + stdin-discarder: 0.1.0 + strip-ansi: 7.1.0 + wcwidth: 1.0.1 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.1 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-cache-control@1.0.1: {} + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + pend@1.2.0: {} + + pg-int8@1.0.1: {} + + pg-protocol@1.10.3: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + possible-typed-array-names@1.1.0: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + prelude-ls@1.2.1: {} + + proc-log@5.0.0: {} + + progress@2.0.3: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + puppeteer-core@24.16.1: + dependencies: + '@puppeteer/browsers': 2.10.6 + chromium-bidi: 7.3.1(devtools-protocol@0.0.1475386) + debug: 4.4.1 + devtools-protocol: 0.0.1475386 + typed-query-selector: 2.12.0 + ws: 8.18.3 + transitivePeerDependencies: + - bare-buffer + - bufferutil + - supports-color + - utf-8-validate + + puppeteer@24.16.1(typescript@5.9.2): + dependencies: + '@puppeteer/browsers': 2.10.6 + chromium-bidi: 7.3.1(devtools-protocol@0.0.1475386) + cosmiconfig: 9.0.0(typescript@5.9.2) + devtools-protocol: 0.0.1475386 + puppeteer-core: 24.16.1 + typed-query-selector: 2.12.0 + transitivePeerDependencies: + - bare-buffer + - bufferutil + - supports-color + - typescript + - utf-8-validate + + queue-microtask@1.2.3: {} + + react-chartjs-2@5.3.0(chart.js@4.5.0)(react@19.1.1): + dependencies: + chart.js: 4.5.0 + react: 19.1.1 + + react-circular-progressbar@2.2.0(react@19.1.1): + dependencies: + react: 19.1.1 + + react-dom@19.1.1(react@19.1.1): + dependencies: + react: 19.1.1 + scheduler: 0.26.0 + + react-hook-form@7.62.0(react@19.1.1): + dependencies: + react: 19.1.1 + + react-intersection-observer@9.16.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + react: 19.1.1 + optionalDependencies: + react-dom: 19.1.1(react@19.1.1) + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-smooth@4.0.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + fast-equals: 5.2.2 + prop-types: 15.8.1 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-transition-group: 4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + + react-transition-group@4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + '@babel/runtime': 7.28.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + + react@19.1.1: {} + + read-cmd-shim@5.0.0: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-directory@2.1.1: {} + + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.1 + module-details-from-path: 1.0.4 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + reusify@1.1.0: {} + + robots-parser@3.0.1: {} + + rrweb-cssom@0.8.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.26.0: {} + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + set-cookie-parser@2.7.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + sharp@0.34.3: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + shimmer@1.2.1: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + + sisteransi@1.0.5: {} + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + socks: 2.8.6 + transitivePeerDependencies: + - supports-color + + socks@2.8.6: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + + source-map-js@1.2.1: {} + + source-map@0.6.1: + optional: true + + speedline-core@1.4.3: + dependencies: + '@types/node': 20.19.10 + image-ssim: 0.2.0 + jpeg-js: 0.4.4 + + sprintf-js@1.1.3: {} + + stable-hash@0.0.5: {} + + stdin-discarder@0.1.0: + dependencies: + bl: 5.1.0 + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + streamx@2.22.1: + dependencies: + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + optionalDependencies: + bare-events: 2.6.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@3.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + stubborn-fs@1.2.5: {} + + styled-jsx@5.1.6(react@19.1.1): + dependencies: + client-only: 0.0.1 + react: 19.1.1 + + supabase@2.33.9: + dependencies: + bin-links: 5.0.0 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + tar: 7.4.3 + transitivePeerDependencies: + - supports-color + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + tailwind-merge@3.3.1: {} + + tailwindcss-animate@1.0.7(tailwindcss@4.1.11): + dependencies: + tailwindcss: 4.1.11 + + tailwindcss@4.1.11: {} + + tapable@2.2.2: {} + + tar-fs@3.1.0: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.1.6 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-buffer + + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.22.1 + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + text-decoder@1.2.3: + dependencies: + b4a: 1.6.7 + + third-party-web@0.27.0: {} + + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + + tldts-core@6.1.86: {} + + tldts-icann@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@0.0.3: {} + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-api-utils@2.1.0(typescript@5.9.2): + dependencies: + typescript: 5.9.2 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.41.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typed-query-selector@2.12.0: {} + + typescript@5.9.2: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + universalify@2.0.1: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.3 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + update-browserslist-db@1.1.3(browserslist@4.25.2): + dependencies: + browserslist: 4.25.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-streams-polyfill@3.3.3: {} + + webidl-conversions@3.0.1: {} + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + when-exit@2.1.4: {} + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@6.0.0: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + ws@7.5.10: {} + + ws@8.18.3: {} + + xdg-basedir@5.1.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@5.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} + + zod@3.25.76: {} diff --git a/website-monitoring-frontend/postcss.config.mjs b/website-monitoring-frontend/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/website-monitoring-frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/website-monitoring-frontend/public/file.svg b/website-monitoring-frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/website-monitoring-frontend/public/file.svg @@ -0,0 +1 @@ +<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg> \ No newline at end of file diff --git a/website-monitoring-frontend/public/globe.svg b/website-monitoring-frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/website-monitoring-frontend/public/globe.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg> \ No newline at end of file diff --git a/website-monitoring-frontend/public/images/wavy_lines.svg b/website-monitoring-frontend/public/images/wavy_lines.svg new file mode 100644 index 0000000..e3626dd --- /dev/null +++ b/website-monitoring-frontend/public/images/wavy_lines.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"> + <path fill="#0099ff" fill-opacity="1" d="M0,160L48,170.7C96,181,192,203,288,197.3C384,192,480,160,576,138.7C672,117,768,107,864,128C960,149,1056,203,1152,213.3C1248,224,1344,192,1392,176L1440,160L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path> +</svg> diff --git a/website-monitoring-frontend/public/next.svg b/website-monitoring-frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/website-monitoring-frontend/public/next.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/website-monitoring-frontend/public/vercel.svg b/website-monitoring-frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/website-monitoring-frontend/public/vercel.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg> \ No newline at end of file diff --git a/website-monitoring-frontend/public/window.svg b/website-monitoring-frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/website-monitoring-frontend/public/window.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg> \ No newline at end of file diff --git a/website-monitoring-frontend/scanner-worker/Dockerfile b/website-monitoring-frontend/scanner-worker/Dockerfile new file mode 100644 index 0000000..f0e76b7 --- /dev/null +++ b/website-monitoring-frontend/scanner-worker/Dockerfile @@ -0,0 +1,21 @@ +FROM node:20-slim + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm install + +# Install Chromium for Lighthouse +RUN apt-get update && \ + apt-get install -y wget ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libcups2 libdbus-1-3 libgdk-pixbuf2.0-0 libnspr4 libnss3 libx11-xcb1 libxcomposite1 libxdamage1 libxrandr2 xdg-utils chromium && \ + ln -s /usr/bin/chromium /usr/bin/chromium-browser + +COPY . . + +RUN npm install -g typescript + +RUN tsc + +EXPOSE 5001 + +CMD ["node", "dist/express-worker.js"] diff --git a/website-monitoring-frontend/scanner-worker/express-worker.ts b/website-monitoring-frontend/scanner-worker/express-worker.ts new file mode 100644 index 0000000..b3f16a7 --- /dev/null +++ b/website-monitoring-frontend/scanner-worker/express-worker.ts @@ -0,0 +1,171 @@ +import express, { Request, Response } from "express"; +import { launch } from "chrome-launcher"; +import lighthouse from "lighthouse"; +import cors from "cors"; + +const app = express(); +app.use(cors()); +app.use(express.json()); + +// Health check endpoint +app.get("/health", (req: Request, res: Response) => { + res.json({ status: "ok", timestamp: new Date().toISOString() }); +}); + +// Enhanced lighthouse endpoint +app.post("/lighthouse", async (req: Request, res: Response) => { + const { url } = req.body; + if (!url) return res.status(400).json({ error: "Missing URL" }); + + console.log(`Starting Lighthouse scan for: ${url}`); + + try { + // Validate URL format + try { + new URL(url); + } catch (urlError) { + return res.status(400).json({ error: "Invalid URL format" }); + } + + const chrome = await launch({ + chromeFlags: [ + "--headless", + "--no-sandbox", + "--disable-dev-shm-usage", + "--disable-gpu", + "--no-first-run", + "--disable-extensions", + "--disable-background-timer-throttling", + "--disable-backgrounding-occluded-windows", + "--disable-renderer-backgrounding", + "--disable-features=TranslateUI", + "--disable-ipc-flooding-protection", + ], + }); + + console.log(`Chrome launched on port: ${chrome.port}`); + + const runnerResult = await lighthouse(url, { + port: chrome.port, + output: "json", + logLevel: "info", + onlyCategories: ["performance", "seo", "accessibility", "best-practices"], + formFactor: "desktop", + screenEmulation: { + mobile: false, + width: 1350, + height: 940, + deviceScaleFactor: 1, + disabled: false, + }, + throttling: { + rttMs: 40, + throughputKbps: 10240, + cpuSlowdownMultiplier: 1, + requestLatencyMs: 0, + downloadThroughputKbps: 0, + uploadThroughputKbps: 0, + }, + }); + + await chrome.kill(); + + if (!runnerResult) throw new Error("Lighthouse returned no result"); + + const { categories, audits } = runnerResult.lhr; + + // Extract key metrics for easier access + const metrics = { + performance: categories.performance?.score + ? Math.round(categories.performance.score * 100) + : null, + seo: categories.seo?.score + ? Math.round(categories.seo.score * 100) + : null, + accessibility: categories.accessibility?.score + ? Math.round(categories.accessibility.score * 100) + : null, + bestPractices: categories["best-practices"]?.score + ? Math.round(categories["best-practices"].score * 100) + : null, + firstContentfulPaint: + audits["first-contentful-paint"]?.numericValue || null, + largestContentfulPaint: + audits["largest-contentful-paint"]?.numericValue || null, + cumulativeLayoutShift: + audits["cumulative-layout-shift"]?.numericValue || null, + totalBlockingTime: audits["total-blocking-time"]?.numericValue || null, + speedIndex: audits["speed-index"]?.numericValue || null, + }; + + console.log( + `Lighthouse scan completed for: ${url} - Performance: ${metrics.performance}%`, + ); + + res.json({ + categories, + audits, + metrics, + raw: runnerResult.lhr, + timestamp: new Date().toISOString(), + url: url, + }); + } catch (error) { + console.error(`Lighthouse scan failed for ${url}:`, error); + res.status(500).json({ + error: error instanceof Error ? error.message : String(error), + url: url, + timestamp: new Date().toISOString(), + }); + } +}); + +// Batch lighthouse endpoint for multiple URLs +app.post("/lighthouse/batch", async (req: Request, res: Response) => { + const { urls } = req.body; + if (!urls || !Array.isArray(urls) || urls.length === 0) { + return res.status(400).json({ error: "Missing or empty URLs array" }); + } + + if (urls.length > 10) { + return res.status(400).json({ error: "Maximum 10 URLs allowed per batch" }); + } + + console.log(`Starting batch Lighthouse scan for ${urls.length} URLs`); + + const results = []; + + for (const url of urls) { + try { + // Make internal request to single lighthouse endpoint + const response = await fetch(`http://localhost:${PORT}/lighthouse`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url }), + }); + + const result = await response.json(); + results.push({ url, success: response.ok, data: result }); + } catch (error) { + results.push({ + url, + success: false, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + res.json({ + results, + total: urls.length, + successful: results.filter((r) => r.success).length, + failed: results.filter((r) => !r.success).length, + timestamp: new Date().toISOString(), + }); +}); + +const PORT = process.env.PORT || 5001; +app.listen(PORT, () => { + console.log(`Lighthouse worker listening on http://localhost:${PORT}`); + console.log(`Health check available at http://localhost:${PORT}/health`); +}); diff --git a/website-monitoring-frontend/scanner-worker/package-lock.json b/website-monitoring-frontend/scanner-worker/package-lock.json new file mode 100644 index 0000000..8ead0f3 --- /dev/null +++ b/website-monitoring-frontend/scanner-worker/package-lock.json @@ -0,0 +1,3062 @@ +{ + "name": "scanner-worker", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scanner-worker", + "version": "1.0.0", + "dependencies": { + "@supabase/supabase-js": "^2.50.0", + "chrome-launcher": "^0.15.0", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^4.18.2", + "lighthouse": "^12.6.1" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^4.17.21", + "@types/node": "^20.0.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz", + "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.1", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz", + "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/icu-skeleton-parser": "1.8.14", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz", + "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", + "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@paulirish/trace_engine": { + "version": "0.0.53", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.53.tgz", + "integrity": "sha512-PUl/vlfo08Oj804VI5nDPeSk9vyslnBlVzDDwFt8SUVxY8+KdGMkra/vrXjEEHe8gb7+RqVTfOIlGw0nyrEelA==", + "license": "BSD-3-Clause", + "dependencies": { + "legacy-javascript": "latest", + "third-party-web": "latest" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", + "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.0.8", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.3.tgz", + "integrity": "sha512-Ausx+Jw1pAMbIBHStoQ6ZqDZR60PsCByvHdw/jdH9AqPrNE9xlBSf9EwcycvmrzwyKspSLaB52grlje2cRIUMg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.3.tgz", + "integrity": "sha512-vyy11fCGpkGK3qI5DSXOjgIboBZTriw0YDx/0KyX5CjIjDDNgp5AGgpgFkfZyiYiaU2Ww3iFuKo4wHmBusz1uA==", + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/integrations": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.3.tgz", + "integrity": "sha512-6i/lYp0BubHPDTg91/uxHvNui427df9r17SsIEXa2eKDwQ9gW2qRx5IWgvnxs2GV/GfSbwcx4swUB3RfEWrXrQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3", + "localforage": "^1.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.3.tgz", + "integrity": "sha512-t+QtekZedEfiZjbkRAk1QWJPnJlFBH/ti96tQhEq7wmlk3VszDXraZvLWZA0P2vXyglKzbWRGkT31aD3/kX+5Q==", + "license": "MIT", + "dependencies": { + "@sentry-internal/tracing": "7.120.3", + "@sentry/core": "7.120.3", + "@sentry/integrations": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/types": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.3.tgz", + "integrity": "sha512-C4z+3kGWNFJ303FC+FxAd4KkHvxpNFYAFN8iMIgBwJdpIl25KZ8Q/VdGn0MLLUEHNLvjob0+wvwlcRBBNLXOow==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.3.tgz", + "integrity": "sha512-UDAOQJtJDxZHQ5Nm1olycBIsz2wdGX8SdzyGVHmD8EOQYAeDZQyIlQYohDe9nazdIOQLZCIc3fU0G9gqVLkaGQ==", + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.70.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.70.0.tgz", + "integrity": "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", + "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.11.10", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.10.tgz", + "integrity": "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.13", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/realtime-js/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.0.tgz", + "integrity": "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.70.0", + "@supabase/functions-js": "2.4.4", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.19.4", + "@supabase/realtime-js": "2.11.10", + "@supabase/storage-js": "2.7.1" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz", + "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz", + "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-bidi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", + "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/csp_evaluator": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.5.tgz", + "integrity": "sha512-EL/iN9etCTzw/fBnp0/uj0f5BOOGvZut2mzsiiBZ/FdT6gFQCKRO/tmcKOxn5drWZ2Ndm/xBb1SI4zwWbGtmIw==", + "license": "Apache-2.0" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "license": "MIT" + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1467305", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1467305.tgz", + "integrity": "sha512-LxwMLqBoPPGpMdRL4NkLFRNy3QLp6Uqa7GNp1v6JaBheop2QrB9Q7q0A/q/CYYP9sBfZdHOyszVx4gc9zyk7ow==", + "license": "BSD-3-Clause" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-link-header": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.3.tgz", + "integrity": "sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "license": "MIT" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/intl-messageformat": { + "version": "10.7.16", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", + "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.2", + "tslib": "^2.8.0" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/js-library-detector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", + "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/legacy-javascript": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.1.tgz", + "integrity": "sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg==", + "license": "Apache-2.0" + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lighthouse": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.6.1.tgz", + "integrity": "sha512-85WDkjcXAVdlFem9Y6SSxqoKiz/89UsDZhLpeLJIsJ4LlHxw047XTZhlFJmjYCB7K5S1erSBAf5cYLcfyNbH3A==", + "license": "Apache-2.0", + "dependencies": { + "@paulirish/trace_engine": "0.0.53", + "@sentry/node": "^7.0.0", + "axe-core": "^4.10.3", + "chrome-launcher": "^1.2.0", + "configstore": "^5.0.1", + "csp_evaluator": "1.1.5", + "devtools-protocol": "0.0.1467305", + "enquirer": "^2.3.6", + "http-link-header": "^1.1.1", + "intl-messageformat": "^10.5.3", + "jpeg-js": "^0.4.4", + "js-library-detector": "^6.7.0", + "lighthouse-logger": "^2.0.1", + "lighthouse-stack-packs": "1.12.2", + "lodash-es": "^4.17.21", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.3.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "puppeteer-core": "^24.10.0", + "robots-parser": "^3.0.1", + "semver": "^5.3.0", + "speedline-core": "^1.4.3", + "third-party-web": "^0.26.6", + "tldts-icann": "^6.1.16", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "chrome-debug": "core/scripts/manual-chrome-launcher.js", + "lighthouse": "cli/index.js", + "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" + }, + "engines": { + "node": ">=18.20" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-stack-packs": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.2.tgz", + "integrity": "sha512-Ug8feS/A+92TMTCK6yHYLwaFMuelK/hAKRMdldYkMNwv+d9PtWxjXEg6rwKtsUXTADajhdrhXyuNCJ5/sfmPFw==", + "license": "Apache-2.0" + }, + "node_modules/lighthouse/node_modules/chrome-launcher": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.0.tgz", + "integrity": "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.cjs" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/lighthouse/node_modules/lighthouse-logger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", + "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/metaviewport-parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer-core": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.0.tgz", + "integrity": "sha512-xX0QJRc8t19iAwRDsAOR38Q/Zx/W6WVzJCEhKCAwp2XMsaWqfNtQ+rBfQW9PlF+Op24d7c8Zlgq9YNmbnA7hdQ==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1452169", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1452169", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz", + "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==", + "license": "BSD-3-Clause" + }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", + "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", + "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/third-party-web": { + "version": "0.26.6", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.26.6.tgz", + "integrity": "sha512-GsjP92xycMK8qLTcQCacgzvffYzEqe29wyz3zdKVXlfRD5Kz1NatCTOZEeDaSd6uCZXvGd2CNVtQ89RNIhJWvA==", + "license": "MIT" + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "node_modules/tldts-icann": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-6.1.86.tgz", + "integrity": "sha512-NFxmRT2lAEMcCOBgeZ0NuM0zsK/xgmNajnY6n4S1mwAKocft2s2ise1O3nQxrH3c+uY6hgHUV9GGNVp7tUE4Sg==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.25.62", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.62.tgz", + "integrity": "sha512-YCxsr4DmhPcrKPC9R1oBHQNlQzlJEyPAId//qTau/vBee9uO8K6prmRq4eMkOyxvBfH4wDPIPdLx9HVMWIY3xA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/website-monitoring-frontend/scanner-worker/package.json b/website-monitoring-frontend/scanner-worker/package.json new file mode 100644 index 0000000..8f9c5fd --- /dev/null +++ b/website-monitoring-frontend/scanner-worker/package.json @@ -0,0 +1,23 @@ +{ + "name": "scanner-worker", + "version": "1.0.0", + "main": "express-worker.ts", + "scripts": { + "start": "ts-node express-worker.ts" + }, + "dependencies": { + "@supabase/supabase-js": "^2.50.0", + "chrome-launcher": "^0.15.0", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^4.18.2", + "lighthouse": "^12.6.1" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^4.17.21", + "@types/node": "^20.0.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.0" + } +} diff --git a/website-monitoring-frontend/scanner-worker/scan-worker.ts b/website-monitoring-frontend/scanner-worker/scan-worker.ts new file mode 100644 index 0000000..d340c28 --- /dev/null +++ b/website-monitoring-frontend/scanner-worker/scan-worker.ts @@ -0,0 +1,94 @@ +import { createClient } from "@supabase/supabase-js"; +import { launch } from "chrome-launcher"; +import lighthouse from "lighthouse"; +import dotenv from "dotenv"; + +dotenv.config(); + +const supabase = createClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_SERVICE_KEY!, +); + +async function main() { + // 1. Hole alle offenen Scans + const { data: scans, error: scanError } = await supabase + .from("scans") + .select("id, page_id") + .eq("status", "pending"); + + if (scanError) { + console.error("Fehler beim Laden der Scans:", scanError); + process.exit(1); + } + + for (const scan of scans ?? []) { + try { + // 2. Hole die URL der Seite + const { data: page, error: pageError } = await supabase + .from("pages") + .select("url") + .eq("id", scan.page_id) + .single(); + + if (pageError || !page) { + console.error("Fehler beim Laden der Seite:", pageError); + continue; + } + + // 3. Setze Scan-Status auf "running" + await supabase + .from("scans") + .update({ status: "running" }) + .eq("id", scan.id); + + // 4. Starte Lighthouse + const chrome = await launch({ + chromeFlags: ["--headless", "--no-sandbox", "--disable-dev-shm-usage"], + }); + + const runnerResult = await lighthouse(page.url, { + port: chrome.port, + output: "json", + logLevel: "info", + onlyCategories: [ + "performance", + "seo", + "accessibility", + "best-practices", + ], + }); + + await chrome.kill(); + + if (!runnerResult) throw new Error("Lighthouse returned no result"); + + // 5. Speichere Ergebnisse + await supabase.from("scan_results").insert([ + { + scan_id: scan.id, + raw_data: runnerResult.lhr, + }, + ]); + + // 6. Setze Scan-Status auf "completed" + await supabase + .from("scans") + .update({ status: "completed" }) + .eq("id", scan.id); + + console.log(`Scan für ${page.url} abgeschlossen.`); + } catch (error) { + console.error("Scan-Fehler:", error); + await supabase + .from("scans") + .update({ + status: "failed", + error_message: error instanceof Error ? error.message : String(error), + }) + .eq("id", scan.id); + } + } +} + +main(); diff --git a/website-monitoring-frontend/scanner-worker/tsconfig.json b/website-monitoring-frontend/scanner-worker/tsconfig.json new file mode 100644 index 0000000..1cedddb --- /dev/null +++ b/website-monitoring-frontend/scanner-worker/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "outDir": "dist", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["*.ts"] +} diff --git a/website-monitoring-frontend/scripts/setup-cron.sh b/website-monitoring-frontend/scripts/setup-cron.sh new file mode 100755 index 0000000..73e349c --- /dev/null +++ b/website-monitoring-frontend/scripts/setup-cron.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# Lighthouse Scanner Cron Job Setup Script +# This script helps you set up automated scanning for your website monitoring application + +set -e + +echo "🚀 Lighthouse Scanner Cron Job Setup" +echo "=====================================" + +# Check if we're in the right directory +if [ ! -f "package.json" ]; then + echo "❌ Error: Please run this script from your project root directory" + exit 1 +fi + +# Check if vercel.json exists +if [ -f "vercel.json" ]; then + echo "✅ Found vercel.json - Vercel cron jobs are configured" + echo " Schedule: Every 6 hours" + echo " Endpoint: /api/cron/scan?mode=all" + echo "" + echo "📝 To deploy with Vercel cron jobs:" + echo " 1. Deploy to Vercel: vercel --prod" + echo " 2. Cron jobs will start automatically" + echo "" +fi + +# Check if GitHub Actions workflow exists +if [ -f ".github/workflows/cron-scan.yml" ]; then + echo "✅ Found GitHub Actions workflow" + echo " Schedule: Every 6 hours" + echo " File: .github/workflows/cron-scan.yml" + echo "" + echo "📝 To use GitHub Actions:" + echo " 1. Push this repository to GitHub" + echo " 2. Set DEPLOYMENT_URL secret in repository settings" + echo " 3. Workflow will run automatically" + echo "" +fi + +# Check environment variables +echo "🔧 Environment Check:" +if [ -f ".env" ]; then + echo "✅ Found .env file" + + # Check for required Supabase variables + if grep -q "NEXT_PUBLIC_SUPABASE_URL" .env; then + echo "✅ NEXT_PUBLIC_SUPABASE_URL is configured" + else + echo "❌ NEXT_PUBLIC_SUPABASE_URL is missing" + fi + + if grep -q "NEXT_PUBLIC_SUPABASE_ANON_KEY" .env; then + echo "✅ NEXT_PUBLIC_SUPABASE_ANON_KEY is configured" + else + echo "❌ NEXT_PUBLIC_SUPABASE_ANON_KEY is missing" + fi + + if grep -q "SUPABASE_SERVICE_ROLE_KEY" .env; then + echo "✅ SUPABASE_SERVICE_ROLE_KEY is configured" + else + echo "❌ SUPABASE_SERVICE_ROLE_KEY is missing" + fi +else + echo "❌ No .env file found" + echo " Please create .env file with your Supabase configuration" +fi + +echo "" +echo "📚 Documentation:" +echo " - Cron setup guide: docs/cron-setup-guide.md" +echo " - Database setup: setup-database.sql" +echo "" + +echo "🎯 Next Steps:" +echo " 1. Ensure your database is set up with all required tables" +echo " 2. Configure your environment variables" +echo " 3. Deploy your application" +echo " 4. Set up cron jobs using one of the methods above" +echo " 5. Test the system by visiting your dashboard" +echo "" + +echo "🔍 Testing:" +echo " You can test the cron endpoint manually:" +echo " curl -X POST 'https://your-domain.com/api/cron/scan?mode=all'" +echo "" + +echo "✅ Setup complete! Check the documentation for detailed instructions." \ No newline at end of file diff --git a/website-monitoring-frontend/scripts/setup-database.sh b/website-monitoring-frontend/scripts/setup-database.sh new file mode 100644 index 0000000..c3b0457 --- /dev/null +++ b/website-monitoring-frontend/scripts/setup-database.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Setup database for website monitoring frontend +echo "Setting up database..." + +# Start PostgreSQL service if not running +if ! pg_isready -h localhost -p 5432 > /dev/null 2>&1; then + echo "Starting PostgreSQL service..." + devenv up & + sleep 5 +fi + +# Create database and user if they don't exist +psql -h localhost -U postgres -c "CREATE DATABASE website_monitoring;" 2>/dev/null || echo "Database already exists" +psql -h localhost -U postgres -c "CREATE USER website_monitoring WITH PASSWORD 'password';" 2>/dev/null || echo "User already exists" +psql -h localhost -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE website_monitoring TO website_monitoring;" 2>/dev/null + +# Run the schema setup +echo "Running database schema setup..." +psql -h localhost -U website_monitoring -d website_monitoring -f setup-database.sql + +# Add missing column if it doesn't exist +echo "Adding missing scheduled_at column..." +psql -h localhost -U website_monitoring -d website_monitoring -c "ALTER TABLE scans ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMPTZ;" + +echo "Database setup complete!" \ No newline at end of file diff --git a/website-monitoring-frontend/setup-database.sql b/website-monitoring-frontend/setup-database.sql new file mode 100644 index 0000000..979bbd7 --- /dev/null +++ b/website-monitoring-frontend/setup-database.sql @@ -0,0 +1,546 @@ +-- Website Monitoring Frontend - Database Setup Script +-- Run this in your Supabase SQL editor to create all required tables + +-- Enable necessary extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +CREATE EXTENSION IF NOT EXISTS "pg_stat_statements"; +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +------ ENUMS ------ +-- Core enums for status and types +DO $$ BEGIN + CREATE TYPE scan_status AS ENUM ( + 'pending', + 'queued', + 'running', + 'completed', + 'failed', + 'cancelled' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE severity_level AS ENUM ( + 'critical', + 'high', + 'medium', + 'low', + 'info' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE comparison_operator AS ENUM ( + 'less_than', + 'less_than_equal', + 'greater_than', + 'greater_than_equal', + 'equal_to', + 'not_equal_to' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE metric_category AS ENUM ( + 'performance', + 'seo', + 'accessibility', + 'best_practices', + 'security', + 'pwa' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE resource_type AS ENUM ( + 'script', + 'stylesheet', + 'image', + 'font', + 'document', + 'media', + 'other' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE notification_channel AS ENUM ( + 'email', + 'slack', + 'webhook', + 'in_app' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE subscription_tier AS ENUM ( + 'free', + 'starter', + 'professional', + 'enterprise' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE user_role AS ENUM ( + 'owner', + 'admin', + 'editor', + 'viewer' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +------ CORE TABLES ------ +-- Organizations table (if not exists) +CREATE TABLE IF NOT EXISTS organizations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR NOT NULL, + subscription_tier subscription_tier DEFAULT 'free', + subscription_status VARCHAR DEFAULT 'active', + billing_email VARCHAR, + max_websites INTEGER DEFAULT 5, + max_users INTEGER DEFAULT 3, + scan_frequency_minutes INTEGER DEFAULT 60, + settings JSONB DEFAULT '{ + "alert_email_digest": "daily", + "default_scan_depth": 3, + "retention_days": 90, + "enable_competitor_analysis": false + }'::jsonb, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Users table (if not exists) +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email VARCHAR UNIQUE NOT NULL, + name VARCHAR, + organization_id UUID REFERENCES organizations(id), + role user_role DEFAULT 'viewer', + is_active BOOLEAN DEFAULT true, + last_login_at TIMESTAMPTZ, + settings JSONB DEFAULT '{ + "email_notifications": true, + "notification_frequency": "instant", + "dashboard_layout": "default" + }'::jsonb, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Websites table (if not exists) +CREATE TABLE IF NOT EXISTS websites ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID REFERENCES organizations(id) NOT NULL, + base_url VARCHAR NOT NULL, + name VARCHAR NOT NULL, + is_active BOOLEAN DEFAULT true, + crawl_settings JSONB DEFAULT '{ + "max_pages": 100, + "max_depth": 3, + "exclude_patterns": [ + "/admin/*", + "/api/*", + "*.pdf", + "*.jpg", + "*.png" + ], + "include_patterns": ["/*"], + "respect_robots_txt": true, + "crawl_frequency": "daily", + "crawl_timing": "off_peak" + }'::jsonb, + scan_schedule JSONB DEFAULT '{ + "frequency": "hourly", + "time_windows": ["0-6", "20-23"], + "days": ["monday", "tuesday", "wednesday", "thursday", "friday"] + }'::jsonb, + performance_budgets JSONB DEFAULT '{ + "page_weight_kb": 1000, + "max_requests": 100, + "time_to_interactive_ms": 3000, + "first_contentful_paint_ms": 1000 + }'::jsonb, + notifications JSONB DEFAULT '{ + "channels": ["email"], + "thresholds": { + "performance": 90, + "accessibility": 90, + "seo": 90, + "best_practices": 90 + } + }'::jsonb, + last_crawl_at TIMESTAMPTZ, + last_scan_at TIMESTAMPTZ, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(organization_id, base_url) +); + +-- Pages table (MISSING - this is causing the 400 errors) +CREATE TABLE IF NOT EXISTS pages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + url VARCHAR NOT NULL, + path VARCHAR NOT NULL, + title VARCHAR, + description TEXT, + content_hash VARCHAR, + content_type VARCHAR, + status_code INTEGER, + is_active BOOLEAN DEFAULT true, + priority INTEGER DEFAULT 1, + depth INTEGER DEFAULT 0, + parent_page_id UUID REFERENCES pages(id), + discovery_method VARCHAR DEFAULT 'crawl', + last_seen_at TIMESTAMPTZ, + metadata JSONB DEFAULT '{ + "inbound_links": 0, + "outbound_links": 0, + "word_count": 0, + "has_canonical": false, + "is_indexable": true + }'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(website_id, url) +); + +------ METRIC DEFINITIONS ------ +CREATE TABLE IF NOT EXISTS metric_definitions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + key VARCHAR NOT NULL UNIQUE, + name VARCHAR NOT NULL, + description TEXT NOT NULL, + category metric_category NOT NULL, + unit VARCHAR, + is_core_metric BOOLEAN DEFAULT false, + default_threshold NUMERIC, + warning_threshold NUMERIC, + critical_threshold NUMERIC, + direction VARCHAR NOT NULL DEFAULT 'higher_is_better', + weight NUMERIC DEFAULT 1.0, + documentation_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Populate core metrics (if not already populated) +INSERT INTO metric_definitions +(key, name, description, category, unit, is_core_metric, default_threshold, warning_threshold, critical_threshold, direction) +VALUES +-- Core Web Vitals +('performance', 'Performance Score', 'Overall performance score of the website', 'performance', '%', true, 90, 80, 70, 'higher_is_better'), +('accessibility', 'Accessibility Score', 'Overall accessibility score of the website', 'accessibility', '%', true, 90, 80, 70, 'higher_is_better'), +('seo', 'SEO Score', 'Overall SEO score of the website', 'seo', '%', true, 90, 80, 70, 'higher_is_better'), +('bestPractices', 'Best Practices Score', 'Overall best practices score', 'best_practices', '%', true, 90, 80, 70, 'higher_is_better'), + +-- Performance Metrics +('firstContentfulPaint', 'First Contentful Paint', 'Time when the first text or image is painted', 'performance', 'ms', true, 1800, 2500, 4000, 'lower_is_better'), +('largestContentfulPaint', 'Largest Contentful Paint', 'Time when the largest text or image is painted', 'performance', 'ms', true, 2500, 4000, 6000, 'lower_is_better'), +('totalBlockingTime', 'Total Blocking Time', 'Sum of all time periods between FCP and Time to Interactive', 'performance', 'ms', true, 200, 400, 600, 'lower_is_better'), +('cumulativeLayoutShift', 'Cumulative Layout Shift', 'Measures visual stability', 'performance', 'score', true, 0.1, 0.25, 0.4, 'lower_is_better'), +('speedIndex', 'Speed Index', 'How quickly content is visually displayed', 'performance', 'ms', true, 3400, 5800, 8800, 'lower_is_better'), +('interactive', 'Time to Interactive', 'Time to fully interactive', 'performance', 'ms', true, 3800, 7300, 12700, 'lower_is_better'), + +-- Resource Metrics +('totalByteWeight', 'Total Byte Weight', 'Total size of all resources', 'performance', 'bytes', false, 1600000, 2400000, 3200000, 'lower_is_better'), +('serverResponseTime', 'Server Response Time', 'Time for server to respond to main document request', 'performance', 'ms', false, 100, 200, 400, 'lower_is_better'), +('networkRtt', 'Network Round Trip Time', 'Network round trip time', 'performance', 'ms', false, 40, 100, 150, 'lower_is_better'), +('networkServerLatency', 'Network Server Latency', 'Server latency in network requests', 'performance', 'ms', false, 30, 100, 150, 'lower_is_better') +ON CONFLICT (key) DO NOTHING; + +------ SCANS AND RESULTS (MISSING - this is causing the 400 errors) ------ +-- Scans table +CREATE TABLE IF NOT EXISTS scans ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + page_id UUID REFERENCES pages(id) NOT NULL, + triggered_by UUID REFERENCES users(id), + scan_type VARCHAR NOT NULL DEFAULT 'full', + status scan_status DEFAULT 'pending', + priority INTEGER DEFAULT 1, + categories metric_category[] DEFAULT ARRAY['performance', 'seo', 'accessibility', 'best_practices'], + device_type VARCHAR DEFAULT 'desktop', + user_agent VARCHAR, + lighthouse_version VARCHAR, + chrome_version VARCHAR, + environment JSONB DEFAULT '{}'::jsonb, + scheduled_at TIMESTAMPTZ, + started_at TIMESTAMPTZ DEFAULT NOW(), + completed_at TIMESTAMPTZ, + duration_ms INTEGER, + error_message TEXT, + retry_count INTEGER DEFAULT 0, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Scan results table (MISSING - this is causing the 400 errors) +CREATE TABLE IF NOT EXISTS scan_results ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + scan_id UUID REFERENCES scans(id) NOT NULL, + category metric_category NOT NULL, + score NUMERIC, + raw_data JSONB, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Metric values table +CREATE TABLE IF NOT EXISTS metric_values ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + scan_id UUID REFERENCES scans(id) NOT NULL, + metric_id UUID REFERENCES metric_definitions(id) NOT NULL, + value NUMERIC NOT NULL, + raw_value VARCHAR, + unit VARCHAR, + is_passing BOOLEAN, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Resource analysis table +CREATE TABLE IF NOT EXISTS resource_analysis ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + scan_id UUID REFERENCES scans(id) NOT NULL, + resource_type resource_type NOT NULL, + url VARCHAR NOT NULL, + size_bytes INTEGER NOT NULL, + transfer_size_bytes INTEGER, + duration_ms INTEGER, + is_third_party BOOLEAN DEFAULT false, + is_cached BOOLEAN, + compression_ratio NUMERIC, + mime_type VARCHAR, + protocol VARCHAR, + priority VARCHAR, + status_code INTEGER, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +------ ALERTS (MISSING - this is causing the 400 errors) ------ +-- Alert configurations +CREATE TABLE IF NOT EXISTS alert_configurations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + name VARCHAR NOT NULL, + description TEXT, + metric_id UUID REFERENCES metric_definitions(id) NOT NULL, + threshold NUMERIC NOT NULL, + comparison comparison_operator DEFAULT 'less_than', + severity severity_level DEFAULT 'medium', + consecutive_count INTEGER DEFAULT 1, + cooldown_minutes INTEGER DEFAULT 60, + notification_channels notification_channel[] DEFAULT ARRAY['email'], + notification_template TEXT, + is_active BOOLEAN DEFAULT true, + last_triggered_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Alerts table +CREATE TABLE IF NOT EXISTS alerts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + page_id UUID REFERENCES pages(id), + config_id UUID REFERENCES alert_configurations(id), + metric_id UUID REFERENCES metric_definitions(id), + severity severity_level DEFAULT 'medium', + title VARCHAR NOT NULL, + message TEXT NOT NULL, + details JSONB DEFAULT '{}'::jsonb, + status VARCHAR DEFAULT 'open', + acknowledged_by UUID REFERENCES users(id), + acknowledged_at TIMESTAMPTZ, + resolved_at TIMESTAMPTZ, + resolution_note TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +------ CRAWL MANAGEMENT ------ +-- Crawl queue +CREATE TABLE IF NOT EXISTS crawl_queue ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + url VARCHAR NOT NULL, + priority INTEGER DEFAULT 1, + status VARCHAR DEFAULT 'pending', + parent_url VARCHAR, + discovery_depth INTEGER DEFAULT 0, + attempts INTEGER DEFAULT 0, + last_attempt_at TIMESTAMPTZ, + next_attempt_at TIMESTAMPTZ, + error_message TEXT, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Crawl sessions +CREATE TABLE IF NOT EXISTS crawl_sessions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + status VARCHAR DEFAULT 'running', + pages_discovered INTEGER DEFAULT 0, + pages_processed INTEGER DEFAULT 0, + start_url VARCHAR NOT NULL, + max_depth INTEGER, + started_at TIMESTAMPTZ DEFAULT NOW(), + completed_at TIMESTAMPTZ, + error_message TEXT, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +------ INDEXES ------ +-- Performance indexes +CREATE INDEX IF NOT EXISTS idx_scans_website_status ON scans(website_id, status); +CREATE INDEX IF NOT EXISTS idx_scans_created_at ON scans(created_at); +CREATE INDEX IF NOT EXISTS idx_scan_results_scan_id ON scan_results(scan_id); +CREATE INDEX IF NOT EXISTS idx_metric_values_scan_metric ON metric_values(scan_id, metric_id); +CREATE INDEX IF NOT EXISTS idx_pages_website_active ON pages(website_id, is_active); +CREATE INDEX IF NOT EXISTS idx_crawl_queue_status_priority ON crawl_queue(status, priority); +CREATE INDEX IF NOT EXISTS idx_alerts_website_status ON alerts(website_id, status); +CREATE INDEX IF NOT EXISTS idx_resource_analysis_scan ON resource_analysis(scan_id); +CREATE INDEX IF NOT EXISTS idx_metric_values_created_at ON metric_values(created_at); +CREATE INDEX IF NOT EXISTS idx_pages_url_trgm ON pages USING gin (url gin_trgm_ops); + +------ SAMPLE DATA FOR TESTING ------ +-- Insert a sample organization if none exists +INSERT INTO organizations (id, name, subscription_tier, subscription_status) +VALUES ( + '00000000-0000-0000-0000-000000000001', + 'Demo Organization', + 'free', + 'active' +) +ON CONFLICT (id) DO NOTHING; + +-- Insert a sample website if none exists +INSERT INTO websites (id, organization_id, base_url, name, is_active) +VALUES ( + '00000000-0000-0000-0000-000000000002', + '00000000-0000-0000-0000-000000000001', + 'https://example.com', + 'Example Website', + true +) +ON CONFLICT (id) DO NOTHING; + +-- Insert a sample page if none exists +INSERT INTO pages (id, website_id, url, path, title, is_active) +VALUES ( + '00000000-0000-0000-0000-000000000003', + '00000000-0000-0000-0000-000000000002', + 'https://example.com', + '/', + 'Example Homepage', + true +) +ON CONFLICT (id) DO NOTHING; + +-- Insert a sample scan if none exists +INSERT INTO scans (id, website_id, page_id, status, scan_type, device_type) +VALUES ( + '00000000-0000-0000-0000-000000000004', + '00000000-0000-0000-0000-000000000002', + '00000000-0000-0000-0000-000000000003', + 'completed', + 'full', + 'desktop' +) +ON CONFLICT (id) DO NOTHING; + +-- Insert sample scan results +INSERT INTO scan_results (scan_id, category, score, raw_data) +VALUES + ('00000000-0000-0000-0000-000000000004', 'performance', 85, '{"firstContentfulPaint": 1200, "largestContentfulPaint": 2100}'), + ('00000000-0000-0000-0000-000000000004', 'seo', 92, '{"metaDescription": true, "titleTag": true}'), + ('00000000-0000-0000-0000-000000000004', 'accessibility', 88, '{"ariaLabels": 5, "contrastRatio": "4.5:1"}'), + ('00000000-0000-0000-0000-000000000004', 'best_practices', 95, '{"usesHttps": true, "noConsoleErrors": true}') +ON CONFLICT DO NOTHING; + +-- Insert sample metric values +INSERT INTO metric_values (scan_id, metric_id, value, unit, is_passing) +SELECT + '00000000-0000-0000-0000-000000000004', + md.id, + CASE md.key + WHEN 'performance' THEN 85 + WHEN 'seo' THEN 92 + WHEN 'accessibility' THEN 88 + WHEN 'bestPractices' THEN 95 + WHEN 'firstContentfulPaint' THEN 1200 + WHEN 'largestContentfulPaint' THEN 2100 + WHEN 'totalBlockingTime' THEN 150 + WHEN 'cumulativeLayoutShift' THEN 0.05 + ELSE 80 + END, + md.unit, + CASE md.key + WHEN 'performance' THEN true + WHEN 'seo' THEN true + WHEN 'accessibility' THEN true + WHEN 'bestPractices' THEN true + WHEN 'firstContentfulPaint' THEN true + WHEN 'largestContentfulPaint' THEN true + WHEN 'totalBlockingTime' THEN true + WHEN 'cumulativeLayoutShift' THEN true + ELSE true + END +FROM metric_definitions md +WHERE md.key IN ('performance', 'seo', 'accessibility', 'bestPractices', 'firstContentfulPaint', 'largestContentfulPaint', 'totalBlockingTime', 'cumulativeLayoutShift') +ON CONFLICT DO NOTHING; + +------ ROW LEVEL SECURITY ------ +-- Enable RLS on all tables +ALTER TABLE organizations ENABLE ROW LEVEL SECURITY; +ALTER TABLE users ENABLE ROW LEVEL SECURITY; +ALTER TABLE websites ENABLE ROW LEVEL SECURITY; +ALTER TABLE pages ENABLE ROW LEVEL SECURITY; +ALTER TABLE scans ENABLE ROW LEVEL SECURITY; +ALTER TABLE scan_results ENABLE ROW LEVEL SECURITY; +ALTER TABLE metric_values ENABLE ROW LEVEL SECURITY; +ALTER TABLE alerts ENABLE ROW LEVEL SECURITY; +ALTER TABLE alert_configurations ENABLE ROW LEVEL SECURITY; +ALTER TABLE crawl_queue ENABLE ROW LEVEL SECURITY; +ALTER TABLE crawl_sessions ENABLE ROW LEVEL SECURITY; + +-- Basic RLS policies (you may need to adjust these based on your auth setup) +CREATE POLICY "Enable read access for authenticated users" ON organizations FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON users FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON websites FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON pages FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON scans FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON scan_results FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON metric_values FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON alerts FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON alert_configurations FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON crawl_queue FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON crawl_sessions FOR SELECT USING (true); + +-- Success message +SELECT 'Database setup completed successfully! All required tables have been created.' as status; \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/api/analyze/route.ts b/website-monitoring-frontend/src/app/api/analyze/route.ts new file mode 100644 index 0000000..b7e66da --- /dev/null +++ b/website-monitoring-frontend/src/app/api/analyze/route.ts @@ -0,0 +1,312 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; + +export async function POST(request: Request) { + try { + const { pageId, websiteId, triggerType = "manual" } = await request.json(); + + if (!pageId && !websiteId) { + return NextResponse.json( + { error: "Either pageId or websiteId must be provided" }, + { status: 400 }, + ); + } + + let pagesToScan = []; + + if (pageId) { + // Scan specific page + const { data: page, error: pageError } = await getSupabaseAdmin() + .from("pages") + .select("id, url, website_id") + .eq("id", pageId) + .single(); + + if (pageError || !page) { + return NextResponse.json({ error: "Page not found" }, { status: 404 }); + } + + pagesToScan = [page]; + } else { + // Scan all active pages for website + const { data: pages, error: pagesError } = await getSupabaseAdmin() + .from("pages") + .select("id, url, website_id") + .eq("website_id", websiteId) + .eq("is_active", true); + + if (pagesError) { + return NextResponse.json( + { error: "Failed to fetch pages" }, + { status: 500 }, + ); + } + + pagesToScan = pages || []; + } + + if (pagesToScan.length === 0) { + return NextResponse.json( + { message: "No active pages found to scan" }, + { status: 200 }, + ); + } + + // Create scans for all pages + const scanPromises = pagesToScan.map(async (page) => { + const { data: scan, error: scanError } = await getSupabaseAdmin() + .from("scans") + .insert([ + { + page_id: page.id, + status: "pending", + trigger_type: triggerType, + scheduled_at: new Date().toISOString(), + }, + ]) + .select() + .single(); + + if (scanError) { + console.error(`Failed to create scan for page ${page.id}:`, scanError); + return null; + } + + // Trigger Lighthouse scan + try { + await triggerLighthouseScan(scan.id as string, page.url as string); + } catch (error) { + console.error( + `Failed to trigger Lighthouse scan for ${page.url}:`, + error, + ); + // Mark scan as failed + await getSupabaseAdmin() + .from("scans") + .update({ + status: "failed", + error_message: error instanceof Error ? error.message : "Unknown error", + completed_at: new Date().toISOString(), + }) + .eq("id", scan.id as string); + } + + return scan; + }); + + const scans = await Promise.all(scanPromises); + const successfulScans = scans.filter((scan) => scan !== null); + + return NextResponse.json({ + success: true, + message: `Started ${successfulScans.length} scans`, + scanIds: successfulScans.map((scan) => scan.id), + totalPages: pagesToScan.length, + successfulScans: successfulScans.length, + }); + } catch (error) { + console.error("Analysis error:", error); + return NextResponse.json( + { + error: + "Failed to start analysis: " + + (error instanceof Error ? error.message : "Unknown error"), + }, + { status: 500 }, + ); + } +} + +async function triggerLighthouseScan(scanId: string, url: string) { + // Update scan status to running + await getSupabaseAdmin() + .from("scans") + .update({ + status: "running", + started_at: new Date().toISOString(), + }) + .eq("id", scanId); + + try { + // Call Lighthouse service + const lighthouseUrl = + process.env.LIGHTHOUSE_SERVICE_URL || "http://localhost:5001"; + const response = await fetch(`${lighthouseUrl}/lighthouse`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ url }), + }); + + if (!response.ok) { + throw new Error( + `Lighthouse service responded with ${response.status}: ${response.statusText}`, + ); + } + + const result = await response.json(); + + // Save scan results + await saveScanResults(scanId, result); + + // Update scan status to completed + await getSupabaseAdmin() + .from("scans") + .update({ + status: "completed", + completed_at: new Date().toISOString(), + }) + .eq("id", scanId); + } catch (error) { + console.error(`Lighthouse scan failed for ${url}:`, error); + + // Update scan status to failed + await getSupabaseAdmin() + .from("scans") + .update({ + status: "failed", + error_message: error instanceof Error ? error.message : "Unknown error", + completed_at: new Date().toISOString(), + }) + .eq("id", scanId); + + throw error; + } +} + +async function saveScanResults(scanId: string, lighthouseResult: any) { + try { + const { categories, audits, raw } = lighthouseResult; + + // Save raw Lighthouse data + await getSupabaseAdmin().from("scan_results").insert([ + { + scan_id: scanId, + raw_data: raw, + performance_score: categories?.performance?.score + ? Math.round(categories.performance.score * 100) + : null, + seo_score: categories?.seo?.score + ? Math.round(categories.seo.score * 100) + : null, + accessibility_score: categories?.accessibility?.score + ? Math.round(categories.accessibility.score * 100) + : null, + best_practices_score: categories?.["best-practices"]?.score + ? Math.round(categories["best-practices"].score * 100) + : null, + first_contentful_paint: + audits?.["first-contentful-paint"]?.numericValue || null, + largest_contentful_paint: + audits?.["largest-contentful-paint"]?.numericValue || null, + cumulative_layout_shift: + audits?.["cumulative-layout-shift"]?.numericValue || null, + total_blocking_time: + audits?.["total-blocking-time"]?.numericValue || null, + speed_index: audits?.["speed-index"]?.numericValue || null, + }, + ]); + + // Extract and save metric values + const metricValues = []; + + // Core Web Vitals + if (audits?.["first-contentful-paint"]?.numericValue) { + metricValues.push({ + scan_id: scanId, + metric_key: "first_contentful_paint", + value: audits["first-contentful-paint"].numericValue, + score: audits["first-contentful-paint"].score, + }); + } + + if (audits?.["largest-contentful-paint"]?.numericValue) { + metricValues.push({ + scan_id: scanId, + metric_key: "largest_contentful_paint", + value: audits["largest-contentful-paint"].numericValue, + score: audits["largest-contentful-paint"].score, + }); + } + + if (audits?.["cumulative-layout-shift"]?.numericValue !== undefined) { + metricValues.push({ + scan_id: scanId, + metric_key: "cumulative_layout_shift", + value: audits["cumulative-layout-shift"].numericValue, + score: audits["cumulative-layout-shift"].score, + }); + } + + if (audits?.["total-blocking-time"]?.numericValue) { + metricValues.push({ + scan_id: scanId, + metric_key: "total_blocking_time", + value: audits["total-blocking-time"].numericValue, + score: audits["total-blocking-time"].score, + }); + } + + // Category scores + if (categories?.performance?.score !== undefined) { + metricValues.push({ + scan_id: scanId, + metric_key: "performance_score", + value: Math.round(categories.performance.score * 100), + score: categories.performance.score, + }); + } + + if (categories?.seo?.score !== undefined) { + metricValues.push({ + scan_id: scanId, + metric_key: "seo_score", + value: Math.round(categories.seo.score * 100), + score: categories.seo.score, + }); + } + + if (categories?.accessibility?.score !== undefined) { + metricValues.push({ + scan_id: scanId, + metric_key: "accessibility_score", + value: Math.round(categories.accessibility.score * 100), + score: categories.accessibility.score, + }); + } + + if (categories?.["best-practices"]?.score !== undefined) { + metricValues.push({ + scan_id: scanId, + metric_key: "best_practices_score", + value: Math.round(categories["best-practices"].score * 100), + score: categories["best-practices"].score, + }); + } + + if (metricValues.length > 0) { + await getSupabaseAdmin().from("metric_values").insert(metricValues); + } + + // Save resource analysis if available + if (raw?.audits?.["resource-summary"]?.details?.items) { + const resources = raw.audits["resource-summary"].details.items.map( + (item: any) => ({ + scan_id: scanId, + resource_type: item.resourceType || "other", + count: item.requestCount || 0, + size_bytes: item.size || 0, + transfer_size_bytes: item.transferSize || 0, + }), + ); + + await getSupabaseAdmin().from("resource_analysis").insert(resources); + } + + console.log(`Successfully saved scan results for scan ${scanId}`); + } catch (error) { + console.error(`Failed to save scan results for scan ${scanId}:`, error); + throw error; + } +} diff --git a/website-monitoring-frontend/src/app/api/competitor-analysis/route.ts b/website-monitoring-frontend/src/app/api/competitor-analysis/route.ts new file mode 100644 index 0000000..a177ab4 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/competitor-analysis/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; +import { supabase } from "@/lib/supabase"; + +export async function GET() { + try { + // Replace this with your actual database query + const { data: competitors, error } = await supabase + .from("competitor_metrics") + .select("*"); + + if (error) throw error; + + // Transform the data to match the CompetitorData type + const transformedData = { + yourSite: { + // Your site's data + }, + competitors: competitors.map((competitor) => ({ + id: competitor.id, + name: competitor.name, + url: competitor.url, + // Transform competitor data + })), + }; + + return NextResponse.json(transformedData); + } catch (error) { + console.log(error); + return NextResponse.json( + { error: "Failed to fetch competitor data" }, + { status: 500 }, + ); + } +} diff --git a/website-monitoring-frontend/src/app/api/crawl/results/route.ts b/website-monitoring-frontend/src/app/api/crawl/results/route.ts new file mode 100644 index 0000000..a07a19a --- /dev/null +++ b/website-monitoring-frontend/src/app/api/crawl/results/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const sessionId = searchParams.get("sessionId"); + if (!sessionId) { + return NextResponse.json({ pages: [] }); + } + + // Hole alle gefundenen Seiten für die Session + const { data: pages } = await getSupabaseAdmin() + .from("pages") + .select("url") + .eq("session_id", sessionId); + + return NextResponse.json({ pages: pages?.map((p) => p.url) || [] }); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/api/crawl/route.ts b/website-monitoring-frontend/src/app/api/crawl/route.ts new file mode 100644 index 0000000..5b7a614 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/crawl/route.ts @@ -0,0 +1,141 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; +import { NewCrawlerService } from "@/services/newCrawlerService"; + +export async function POST(request: Request) { + try { + // Parse request body + let websiteId; + try { + const body = await request.json(); + websiteId = body.websiteId; + + // Normalize the websiteId to ensure consistent format + websiteId = String(websiteId).trim().toLowerCase(); + console.log("Processing website ID:", websiteId); + } catch (parseError) { + console.error("Error parsing request body:", parseError); + return NextResponse.json( + { error: "Invalid request format" }, + { status: 400 }, + ); + } + + // Validate input + if (!websiteId) { + return NextResponse.json( + { error: "Website ID is required" }, + { status: 400 }, + ); + } + + // Fetch website details using admin client (bypasses RLS) + const { data: websites, error: websiteError } = await getSupabaseAdmin() + .from("websites") + .select("id, name, base_url") + .eq("id", websiteId); + + if (websiteError) { + console.error("Website query error:", websiteError); + return NextResponse.json( + { error: `Database error: ${websiteError.message}` }, + { status: 500 }, + ); + } + + // Check if website exists + if (!websites || websites.length === 0) { + console.error("Website not found with ID:", websiteId); + + // Try to find similar websites for debugging + const { data: allWebsites } = await getSupabaseAdmin() + .from("websites") + .select("id, name, base_url"); + + console.log(`Found ${allWebsites?.length || 0} total websites`); + + // Log available IDs for comparison + const availableIds = + allWebsites?.map((w) => String(w.id).toLowerCase()) || []; + console.log("Available website IDs:", availableIds); + + return NextResponse.json({ + error: "Website not found", + debug: { + requestedId: websiteId, + availableIds: availableIds, + totalWebsites: allWebsites?.length || 0 + } + }, { status: 404 }); + } + + // Website found, proceed with crawl + const website = websites[0]; + console.log("Found website:", website.name, website.base_url); + + // Create a new crawl session (using admin client) + const { data: sessions, error: sessionError } = await getSupabaseAdmin() + .from("crawl_sessions") + .insert([ + { + website_id: website.id, // Use the ID from the database + status: "pending", + start_url: website.base_url, + total_urls: 0, + processed_urls: 0, + progress_percentage: 0, + pages_discovered: 0, + pages_processed: 0, + }, + ]) + .select(); + + if (sessionError) { + console.error("Failed to create crawl session:", sessionError); + console.error("Session error details:", { + message: sessionError.message, + details: sessionError.details, + hint: sessionError.hint, + code: sessionError.code + }); + return NextResponse.json( + { + error: "Failed to create crawl session", + details: sessionError.message, + code: sessionError.code + }, + { status: 500 }, + ); + } + + if (!sessions || sessions.length === 0) { + return NextResponse.json( + { error: "Session was created but not returned" }, + { status: 500 }, + ); + } + + const session = sessions[0] as { id: string }; + console.log("Created crawl session:", session.id); + + // Start crawler in background + const crawler = new NewCrawlerService(String(website.id), String(session.id)); + crawler.startCrawl().catch((err) => { + console.error("Crawler error:", err); + }); + + // Return successful response + return NextResponse.json({ + success: true, + message: "Crawl started", + sessionId: session.id, + }); + } catch (error) { + // Catch-all error handler + console.error("Crawl initialization error:", error); + return NextResponse.json( + { error: "Failed to start crawl: " + (error) }, + { status: 500 }, + ); + } +} diff --git a/website-monitoring-frontend/src/app/api/crawl/status/[id]/route.ts b/website-monitoring-frontend/src/app/api/crawl/status/[id]/route.ts new file mode 100644 index 0000000..460fbf4 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/crawl/status/[id]/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; +import { supabase } from "@/lib/supabase"; + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> }, +) { + try { + const { id } = await params; + + const { data, error } = await supabase + .from("crawl_sessions") + .select("*") + .eq("id", id) + .single(); + + if (error) throw error; + + return NextResponse.json(data); + } catch (error) { + console.error("Error fetching crawl status:", error); + return NextResponse.json( + { error: "Failed to fetch crawl status" }, + { status: 500 }, + ); + } +} diff --git a/website-monitoring-frontend/src/app/api/cron/scan/route.ts b/website-monitoring-frontend/src/app/api/cron/scan/route.ts new file mode 100644 index 0000000..c1150b2 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/cron/scan/route.ts @@ -0,0 +1,259 @@ +import { NextResponse } from "next/server"; +import { scanScheduler } from "@/services/scanScheduler"; +import { lighthouseScanner } from "@/services/lighthouseScanner"; +import { logError } from "@/utils/errorUtils"; + +export async function GET(request: Request) { + try { + const url = new URL(request.url); + const mode = url.searchParams.get("mode") || "all"; // "scheduled", "change_detection", "all" + const organizationId = url.searchParams.get("organizationId"); // Optional: limit to specific org + + console.info(JSON.stringify({ level: 'info', event: 'scan_process_start', mode, timestamp: new Date().toISOString() })); + + const results = { + scheduledScans: 0, + changeDetectionScans: 0, + errors: [] as string[], + startTime: new Date().toISOString(), + }; + + // Process scheduled scans + if (mode === "scheduled" || mode === "all") { + try { + console.info(JSON.stringify({ level: 'info', event: 'processing_scheduled_scans', timestamp: new Date().toISOString() })); + await scanScheduler.processScheduledScans(); + + // Get count of processed scans + const scheduledScans = await scanScheduler.getScheduledScans(); + results.scheduledScans = scheduledScans.length; + + console.info(JSON.stringify({ level: 'info', event: 'scheduled_scans_processed', count: results.scheduledScans, timestamp: new Date().toISOString() })); + } catch (error) { + const errorMsg = `Error processing scheduled scans: ${error instanceof Error ? error.message : 'Unknown error'}`; + logError(errorMsg, error); + results.errors.push(errorMsg); + } + } + + // Process change detection + if (mode === "change_detection" || mode === "all") { + try { + console.info(JSON.stringify({ level: 'info', event: 'processing_change_detection', timestamp: new Date().toISOString() })); + await scanScheduler.processChangeDetection(); + + // Note: Change detection count is harder to track since it's based on actual changes + // We'll just indicate it was processed + results.changeDetectionScans = -1; // -1 indicates processed but count unknown + + console.info(JSON.stringify({ level: 'info', event: 'change_detection_processed', timestamp: new Date().toISOString() })); + } catch (error) { + const errorMsg = `Error processing change detection: ${error instanceof Error ? error.message : 'Unknown error'}`; + logError(errorMsg, error); + results.errors.push(errorMsg); + } + } + + // Get overall statistics + const stats = await getScanStatistics(organizationId ?? undefined); + + const response = { + success: results.errors.length === 0, + message: `Automatic scan process completed - ${results.scheduledScans} scheduled scans, change detection processed`, + results, + statistics: stats, + timestamp: new Date().toISOString(), + }; + + console.info(JSON.stringify({ level: 'info', event: 'scan_process_completed', success: response.success, timestamp: new Date().toISOString() })); + + return NextResponse.json(response); + } catch (error) { + const errorMsg = `Critical error in automatic scan process: ${error instanceof Error ? error.message : 'Unknown error'}`; + logError(errorMsg, error); + + return NextResponse.json( + { + success: false, + error: errorMsg, + timestamp: new Date().toISOString(), + }, + { status: 500 } + ); + } +} + +/** + * Get scan statistics for monitoring + */ +async function getScanStatistics(organizationId?: string) { + try { + const { getSupabaseAdmin } = await import("@/lib/admin"); + const supabase = getSupabaseAdmin(); + + const now = new Date(); + const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + // Build query + let query = supabase + .from('scans') + .select('id, status, created_at, triggered_by'); + + if (organizationId) { + const { data: websitesForOrg, error: orgErr } = await supabase + .from('websites') + .select('id') + .eq('organization_id', organizationId); + if (orgErr) { + throw orgErr; + } + const websiteIds = (websitesForOrg || []).map((w: any) => w.id); + if (websiteIds.length === 0) { + return { + today: { total: 0, byStatus: {}, byTrigger: {} }, + thisMonth: { total: 0 }, + last24Hours: { total: 0 }, + }; + } + query = query.in('website_id', websiteIds as any[]); + } + + // Get today's scans + const { data: todayScans } = await query + .gte('created_at', startOfDay.toISOString()); + + // Get this month's scans + const { data: monthScans } = await query + .gte('created_at', startOfMonth.toISOString()); + + // Get scans by status + const { data: statusCounts } = await query + .select('status') as unknown as { data: Array<{ status: string }> }; + + const statusBreakdown = (statusCounts?.reduce((acc: Record<string, number>, scan: { status: string }) => { + const key = String(scan.status || 'unknown'); + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {} as Record<string, number>)) || {}; + + // Get scans by trigger type + const { data: triggerCounts } = await query + .select('triggered_by') as unknown as { data: Array<{ triggered_by: string | null }> }; + + const triggerBreakdown = (triggerCounts?.reduce((acc: Record<string, number>, scan: { triggered_by: string | null }) => { + const trigger = String(scan.triggered_by || 'unknown'); + acc[trigger] = (acc[trigger] || 0) + 1; + return acc; + }, {} as Record<string, number>)) || {}; + + return { + today: { + total: todayScans?.length || 0, + byStatus: statusBreakdown, + byTrigger: triggerBreakdown, + }, + thisMonth: { + total: monthScans?.length || 0, + }, + last24Hours: { + total: todayScans?.length || 0, + }, + }; + } catch (error) { + logError('Error getting scan statistics', error); + return { + today: { total: 0, byStatus: {}, byTrigger: {} }, + thisMonth: { total: 0 }, + last24Hours: { total: 0 }, + }; + } +} + +/** + * Manual scan trigger endpoint + */ +export async function POST(request: Request) { + try { + const body = await request.json(); + const { websiteId, pageId, deviceType = 'desktop', categories, priority = 'medium' } = body; + + if (!websiteId || !pageId) { + return NextResponse.json( + { error: "Website ID and Page ID are required" }, + { status: 400 } + ); + } + + console.info(JSON.stringify({ level: 'info', event: 'manual_scan_triggered', websiteId, pageId, timestamp: new Date().toISOString() })); + + // Check subscription limits + const { data: website } = await (await import("@/lib/admin")).getSupabaseAdmin() + .from('websites') + .select('organization_id') + .eq('id', websiteId) + .single(); + + if (!website) { + return NextResponse.json( + { error: "Website not found" }, + { status: 404 } + ); + } + + const { canScan, limits, currentUsage } = await lighthouseScanner.checkSubscriptionLimits( + String(website.organization_id) + ); + + if (!canScan) { + return NextResponse.json( + { + error: "Subscription limit exceeded", + limits, + currentUsage, + }, + { status: 429 } + ); + } + + // Perform the scan + const scanConfig = { + websiteId, + pageId, + deviceType: deviceType as 'desktop' | 'mobile', + categories: categories || ['performance', 'accessibility', 'seo', 'best_practices'], + priority: priority as 'low' | 'medium' | 'high', + triggeredBy: 'manual' as const, + }; + + const result = await lighthouseScanner.performScan(scanConfig); + + if (result.success) { + return NextResponse.json({ + success: true, + scanId: result.scanId, + message: "Scan completed successfully", + metrics: result.metrics, + }); + } else { + return NextResponse.json( + { + success: false, + error: result.error, + }, + { status: 500 } + ); + } + } catch (error) { + const errorMsg = `Error in manual scan: ${error instanceof Error ? error.message : 'Unknown error'}`; + logError(errorMsg, error); + + return NextResponse.json( + { + success: false, + error: errorMsg, + }, + { status: 500 } + ); + } +} diff --git a/website-monitoring-frontend/src/app/api/debug/crawl-queue/[websiteId]/route.ts b/website-monitoring-frontend/src/app/api/debug/crawl-queue/[websiteId]/route.ts new file mode 100644 index 0000000..9413e2b --- /dev/null +++ b/website-monitoring-frontend/src/app/api/debug/crawl-queue/[websiteId]/route.ts @@ -0,0 +1,86 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + try { + const { websiteId } = await params; + + // Get crawl queue items + const { data: queueItems, error: queueError } = await getSupabaseAdmin() + .from("crawl_queue") + .select("*") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }); + + if (queueError) throw queueError; + + // Get crawl sessions + const { data: sessions, error: sessionsError } = await getSupabaseAdmin() + .from("crawl_sessions") + .select("*") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }); + + if (sessionsError) throw sessionsError; + + // Get pages discovered + const { data: pages, error: pagesError } = await getSupabaseAdmin() + .from("pages") + .select("id, url, title, is_active, depth, created_at, metadata") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }); + + if (pagesError) throw pagesError; + + // Get website info + const { data: website, error: websiteError } = await getSupabaseAdmin() + .from("websites") + .select("*") + .eq("id", websiteId) + .single(); + + if (websiteError) throw websiteError; + + // Statistics + const queueStats = { + total: queueItems?.length || 0, + pending: queueItems?.filter(item => item.status === 'pending').length || 0, + processing: queueItems?.filter(item => item.status === 'processing').length || 0, + completed: queueItems?.filter(item => item.status === 'completed').length || 0, + failed: queueItems?.filter(item => item.status === 'failed').length || 0, + skipped: queueItems?.filter(item => item.status === 'skipped').length || 0, + }; + + const sessionStats = { + total: sessions?.length || 0, + running: sessions?.filter(s => s.status === 'running').length || 0, + completed: sessions?.filter(s => s.status === 'completed').length || 0, + failed: sessions?.filter(s => s.status === 'failed').length || 0, + }; + + const pageStats = { + total: pages?.length || 0, + active: pages?.filter(p => p.is_active).length || 0, + inactive: pages?.filter(p => !p.is_active).length || 0, + }; + + return NextResponse.json({ + website, + queueStats, + sessionStats, + pageStats, + queueItems: queueItems?.slice(0, 20), // Last 20 queue items + sessions: sessions?.slice(0, 5), // Last 5 sessions + pages: pages?.slice(0, 20), // Last 20 pages + }); + } catch (error) { + console.error("Error fetching crawl debug info:", error); + return NextResponse.json( + { error: "Failed to fetch crawl debug info" }, + { status: 500 }, + ); + } +} diff --git a/website-monitoring-frontend/src/app/api/debug/crawl-status/[websiteId]/route.ts b/website-monitoring-frontend/src/app/api/debug/crawl-status/[websiteId]/route.ts new file mode 100644 index 0000000..c1878ab --- /dev/null +++ b/website-monitoring-frontend/src/app/api/debug/crawl-status/[websiteId]/route.ts @@ -0,0 +1,34 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; + +export async function GET( + request: Request, + context: { params: Promise<{ websiteId: string }> }, +) { + try { + const { websiteId } = await context.params; + + // Get crawl sessions for this website + const { data: sessions, error: sessionsError } = await getSupabaseAdmin() + .from("crawl_sessions") + .select("*") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }); + + // Get all pages for this website + const { data: pages, error: pagesError } = await getSupabaseAdmin() + .from("pages") + .select("*") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }); + + return NextResponse.json({ + sessions, + sessionsError, + pages, + pagesError, + }); + } catch (err) { + return NextResponse.json({ error: String(err) }, { status: 500 }); + } +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/api/email-exists/route.ts b/website-monitoring-frontend/src/app/api/email-exists/route.ts new file mode 100644 index 0000000..441b294 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/email-exists/route.ts @@ -0,0 +1,68 @@ +import { createClient } from '@supabase/supabase-js'; +import { getSupabaseAdmin } from "@/lib/admin"; + +// In-memory rate limiter (for demo/dev only; use Redis for production) +const rateLimit: Record<string, { count: number; last: number }> = {}; +const MAX_ATTEMPTS = 10; +const WINDOW_MS = 60 * 1000; // 1 minute + +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! // Use service role for backend +); + +export async function POST(req: Request) { + const ip = req.headers.get('x-forwarded-for') || ''; + const now = Date.now(); + + // Rate limiting + if (!rateLimit[ip]) rateLimit[ip] = { count: 0, last: now }; + if (now - rateLimit[ip].last > WINDOW_MS) { + rateLimit[ip] = { count: 0, last: now }; + } + rateLimit[ip].count += 1; + if (rateLimit[ip].count > MAX_ATTEMPTS) { + return new Response(JSON.stringify({ error: 'Too many requests' }), { + status: 429, + headers: { 'Content-Type': 'application/json' }, + }); + } + + let email: string | undefined; + try { + const body = await req.json(); + email = body.email; + } catch { + return new Response(JSON.stringify({ error: 'Invalid JSON' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (!email || typeof email !== 'string') { + return new Response(JSON.stringify({ error: 'Invalid email' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Debug log incoming email + console.log('API /api/email-exists: email =', email); + + const { data, error } = await supabase.rpc('email_exists', { email_to_check: email }); + + // Debug log Supabase response + console.log('API /api/email-exists: supabase.rpc response =', { data, error }); + + if (error) { + return new Response(JSON.stringify({ error: 'Server error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response(JSON.stringify({ exists: data === true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/api/monitor/start/route.ts b/website-monitoring-frontend/src/app/api/monitor/start/route.ts new file mode 100644 index 0000000..9eb4609 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/monitor/start/route.ts @@ -0,0 +1,45 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; +import { supabase } from "@/lib/supabase"; + +export async function POST(request: Request) { + try { + const { websiteId } = await request.json(); + + // Create a new scan + const { data: scan, error: scanError } = await supabase + .from("scans") + .insert([ + { + website_id: websiteId, + status: "pending", + }, + ]) + .select() + .single(); + + if (scanError) throw scanError; + + // Trigger the analysis process + const response = await fetch("/api/analyze", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + websiteId, + scanId: scan.id, + }), + }); + + if (!response.ok) { + throw new Error("Failed to start analysis"); + } + + return NextResponse.json({ success: true, scanId: scan.id }); + } catch (error) { + console.error("Monitor start error:", error); + return NextResponse.json( + { error: "Failed to start monitoring" }, + { status: 500 }, + ); + } +} diff --git a/website-monitoring-frontend/src/app/api/organization/members/route.ts b/website-monitoring-frontend/src/app/api/organization/members/route.ts new file mode 100644 index 0000000..8940d29 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/organization/members/route.ts @@ -0,0 +1,265 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; + +export async function GET(request: Request) { + try { + const url = new URL(request.url); + const organizationId = url.searchParams.get("organizationId"); + const userId = url.searchParams.get("userId"); + + if (!organizationId || !userId) { + return NextResponse.json({ error: "Organization ID and User ID are required" }, { status: 400 }); + } + + // Verify user has access to this organization + const { data: userOrg, error: accessError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", userId) + .eq("organization_id", organizationId) + .single(); + + if (accessError || !userOrg) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + // Get all members of the organization + const { data: members, error: membersError } = await getSupabaseAdmin() + .from("users") + .select("id, name, email, role, created_at") + .eq("organization_id", organizationId) + .order("created_at", { ascending: true }); + + if (membersError) { + console.error("Error fetching members:", membersError); + return NextResponse.json({ error: "Failed to fetch members" }, { status: 500 }); + } + + return NextResponse.json({ members }); + } catch (error) { + console.error("Error in members GET:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function POST(request: Request) { + try { + const { organizationId, email, role, invitedBy } = await request.json(); + + if (!organizationId || !email || !role || !invitedBy) { + return NextResponse.json({ + error: "Organization ID, email, role, and inviter ID are required" + }, { status: 400 }); + } + + // Verify inviter has permission (must be owner or admin) + const { data: inviter, error: inviterError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", invitedBy) + .eq("organization_id", organizationId) + .single(); + + if (inviterError || !inviter) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + if (inviter.role !== "owner" && inviter.role !== "admin") { + return NextResponse.json({ + error: "Only owners and admins can invite members" + }, { status: 403 }); + } + + // Check if user already exists in the system + const { data: existingUsers, error: userCheckError } = await getSupabaseAdmin() + .auth.admin.listUsers(); + + if (userCheckError) { + console.error("Error checking existing users:", userCheckError); + return NextResponse.json({ error: "Failed to check existing users" }, { status: 500 }); + } + + const existingUser = existingUsers.users.find(u => u.email === email); + + if (existingUser) { + // Check if user is already in an organization + const { data: userRecord } = await getSupabaseAdmin() + .from("users") + .select("organization_id") + .eq("id", existingUser.id) + .single(); + + if (userRecord?.organization_id) { + if (userRecord.organization_id === organizationId) { + return NextResponse.json({ + error: "User is already a member of this organization" + }, { status: 400 }); + } else { + return NextResponse.json({ + error: "User is already a member of another organization" + }, { status: 400 }); + } + } + + // Add existing user to organization + const { error: updateError } = await getSupabaseAdmin() + .from("users") + .update({ organization_id: organizationId, role }) + .eq("id", existingUser.id); + + if (updateError) { + console.error("Error adding existing user to organization:", updateError); + return NextResponse.json({ error: "Failed to add user to organization" }, { status: 500 }); + } + + // Get updated user data + const { data: updatedUser } = await getSupabaseAdmin() + .from("users") + .select("id, name, email, role, created_at") + .eq("id", existingUser.id) + .single(); + + return NextResponse.json({ + member: updatedUser, + message: "Existing user added to organization" + }); + } else { + // Create invitation record for new user + // Note: In a real app, you'd send an email invitation here + // For now, we'll just create a placeholder record + + return NextResponse.json({ + message: "Invitation would be sent to new user", + action: "invitation_sent" + }); + } + } catch (error) { + console.error("Error in members POST:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function PUT(request: Request) { + try { + const { memberId, role, updatedBy, organizationId } = await request.json(); + + if (!memberId || !role || !updatedBy || !organizationId) { + return NextResponse.json({ + error: "Member ID, role, updater ID, and organization ID are required" + }, { status: 400 }); + } + + // Verify updater has permission (must be owner) + const { data: updater, error: updaterError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", updatedBy) + .eq("organization_id", organizationId) + .single(); + + if (updaterError || !updater) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + if (updater.role !== "owner") { + return NextResponse.json({ + error: "Only owners can update member roles" + }, { status: 403 }); + } + + // Don't allow changing the role of the organization owner + const { data: targetMember } = await getSupabaseAdmin() + .from("users") + .select("role") + .eq("id", memberId) + .single(); + + if (targetMember?.role === "owner" && role !== "owner") { + return NextResponse.json({ + error: "Cannot change the role of the organization owner" + }, { status: 400 }); + } + + // Update member role + const { data: updatedMember, error: updateError } = await getSupabaseAdmin() + .from("users") + .update({ role }) + .eq("id", memberId) + .eq("organization_id", organizationId) + .select("id, name, email, role, created_at") + .single(); + + if (updateError) { + console.error("Error updating member role:", updateError); + return NextResponse.json({ error: "Failed to update member role" }, { status: 500 }); + } + + return NextResponse.json({ member: updatedMember }); + } catch (error) { + console.error("Error in members PUT:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function DELETE(request: Request) { + try { + const url = new URL(request.url); + const memberId = url.searchParams.get("memberId"); + const removedBy = url.searchParams.get("removedBy"); + const organizationId = url.searchParams.get("organizationId"); + + if (!memberId || !removedBy || !organizationId) { + return NextResponse.json({ + error: "Member ID, remover ID, and organization ID are required" + }, { status: 400 }); + } + + // Verify remover has permission (must be owner or admin) + const { data: remover, error: removerError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", removedBy) + .eq("organization_id", organizationId) + .single(); + + if (removerError || !remover) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + if (remover.role !== "owner" && remover.role !== "admin") { + return NextResponse.json({ + error: "Only owners and admins can remove members" + }, { status: 403 }); + } + + // Don't allow removing the organization owner + const { data: targetMember } = await getSupabaseAdmin() + .from("users") + .select("role") + .eq("id", memberId) + .single(); + + if (targetMember?.role === "owner") { + return NextResponse.json({ + error: "Cannot remove the organization owner" + }, { status: 400 }); + } + + // Remove member from organization (set organization_id to null) + const { error: removeError } = await getSupabaseAdmin() + .from("users") + .update({ organization_id: null, role: "member" }) + .eq("id", memberId) + .eq("organization_id", organizationId); + + if (removeError) { + console.error("Error removing member:", removeError); + return NextResponse.json({ error: "Failed to remove member" }, { status: 500 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error in members DELETE:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/api/organization/route.ts b/website-monitoring-frontend/src/app/api/organization/route.ts new file mode 100644 index 0000000..19655b3 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/organization/route.ts @@ -0,0 +1,229 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; +import { supabase } from "@/lib/supabase"; +import { createClient } from "@supabase/supabase-js"; + +// Initialize Supabase with service role for admin operations + +export async function POST(request: Request) { + try { + const { userId, name } = await request.json(); + + if (!userId) { + return NextResponse.json( + { error: "User ID is required" }, + { status: 400 }, + ); + } + + // Create organization + const { data: org, error: orgError } = await supabase + .from("organizations") + .insert([ + { + name: name || "My Organization", + subscription_tier: "free", + subscription_status: "active", + }, + ]) + .select() + .single(); + + if (orgError) { + throw orgError; + } + + // Update user with organization ID + const { error: userError } = await supabase + .from("users") + .update({ organization_id: org.id }) + .eq("id", userId); + + if (userError) { + throw userError; + } + + return NextResponse.json({ + success: true, + organization: org, + }); + } catch (error) { + console.error("Organization creation error:", error); + return NextResponse.json( + { error: "Failed to create organization" }, + { status: 500 }, + ); + } +} + +export async function GET(request: Request) { + try { + const url = new URL(request.url); + const userId = url.searchParams.get("userId"); + + if (!userId) { + return NextResponse.json( + { error: "User ID is required" }, + { status: 400 }, + ); + } + + // Get organizations where user is a member + const { data: userOrgs, error: userOrgError } = await getSupabaseAdmin() + .from("users") + .select(` + organization_id, + role, + organizations ( + id, + name, + subscription_tier, + subscription_status, + created_at + ) + `) + .eq("id", userId); + + if (userOrgError) { + console.error("Error fetching user organizations:", userOrgError); + return NextResponse.json({ error: "Failed to fetch organizations" }, { status: 500 }); + } + + // Get organization stats + const orgIds = userOrgs?.map(u => u.organization_id).filter(Boolean) || []; + + if (orgIds.length === 0) { + return NextResponse.json({ organizations: [] }); + } + + const [membersData, websitesData] = await Promise.all([ + // Get member counts + getSupabaseAdmin() + .from("users") + .select("organization_id") + .in("organization_id", orgIds), + + // Get website counts + getSupabaseAdmin() + .from("websites") + .select("organization_id") + .in("organization_id", orgIds) + ]); + + const memberCounts = (membersData.data as Array<{ organization_id: string }> | null | undefined)?.reduce((acc: Record<string, number>, member) => { + const key = String(member.organization_id); + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {} as Record<string, number>) || {}; + + const websiteCounts = (websitesData.data as Array<{ organization_id: string }> | null | undefined)?.reduce((acc: Record<string, number>, website) => { + const key = String(website.organization_id); + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {} as Record<string, number>) || {}; + + const orgsWithStats = (userOrgs as Array<any> | undefined)?.map((userOrg: any) => ({ + id: userOrg.organizations?.id || "", + name: userOrg.organizations?.name || "", + subscription_tier: userOrg.organizations?.subscription_tier || "free", + subscription_status: userOrg.organizations?.subscription_status || "active", + created_at: userOrg.organizations?.created_at || "", + member_count: memberCounts[String(userOrg.organization_id)] || 0, + website_count: websiteCounts[String(userOrg.organization_id)] || 0, + user_role: userOrg.role || "member", + })).filter((org: any) => org.id) || []; + + return NextResponse.json({ organizations: orgsWithStats }); + } catch (error) { + console.error("Error in organization GET:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function PUT(request: Request) { + try { + const { id, name, userId } = await request.json(); + + if (!id || !name || !userId) { + return NextResponse.json({ error: "ID, name, and user ID are required" }, { status: 400 }); + } + + // Check if user has permission to update this organization + const { data: userOrg, error: permissionError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", userId) + .eq("organization_id", id) + .single(); + + if (permissionError || !userOrg) { + return NextResponse.json({ error: "Organization not found or access denied" }, { status: 403 }); + } + + if (userOrg.role !== "owner") { + return NextResponse.json({ error: "Only organization owners can update organizations" }, { status: 403 }); + } + + // Update organization + const { data: org, error: updateError } = await getSupabaseAdmin() + .from("organizations") + .update({ name }) + .eq("id", id) + .select() + .single(); + + if (updateError) { + console.error("Error updating organization:", updateError); + return NextResponse.json({ error: "Failed to update organization" }, { status: 500 }); + } + + return NextResponse.json({ organization: org }); + } catch (error) { + console.error("Error in organization PUT:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function DELETE(request: Request) { + try { + const url = new URL(request.url); + const id = url.searchParams.get("id"); + const userId = url.searchParams.get("userId"); + + if (!id || !userId) { + return NextResponse.json({ error: "ID and user ID are required" }, { status: 400 }); + } + + // Check if user has permission to delete this organization + const { data: userOrg, error: permissionError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", userId) + .eq("organization_id", id) + .single(); + + if (permissionError || !userOrg) { + return NextResponse.json({ error: "Organization not found or access denied" }, { status: 403 }); + } + + if (userOrg.role !== "owner") { + return NextResponse.json({ error: "Only organization owners can delete organizations" }, { status: 403 }); + } + + // Delete organization (CASCADE should handle related records) + const { error: deleteError } = await getSupabaseAdmin() + .from("organizations") + .delete() + .eq("id", id); + + if (deleteError) { + console.error("Error deleting organization:", deleteError); + return NextResponse.json({ error: "Failed to delete organization" }, { status: 500 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error in organization DELETE:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/website-monitoring-frontend/src/app/api/test-lighthouse/route.ts b/website-monitoring-frontend/src/app/api/test-lighthouse/route.ts new file mode 100644 index 0000000..d733e73 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/test-lighthouse/route.ts @@ -0,0 +1,69 @@ +import { NextResponse } from "next/server"; + +export async function POST(request: Request) { + try { + const { url, deviceType = 'desktop' } = await request.json(); + + if (!url) { + return NextResponse.json({ error: "URL is required" }, { status: 400 }); + } + + // Test the Lighthouse scanner directly + const lighthouseUrl = process.env.LIGHTHOUSE_SERVICE_URL || 'http://localhost:5001'; + const response = await fetch(`${lighthouseUrl}/lighthouse`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ url }), + }); + + if (!response.ok) { + throw new Error(`Lighthouse service responded with ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + + return NextResponse.json({ + success: true, + result, + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error('Lighthouse test error:', error); + return NextResponse.json( + { + error: 'Lighthouse test failed: ' + (error instanceof Error ? error.message : 'Unknown error'), + }, + { status: 500 } + ); + } +} + +export async function GET() { + try { + // Test the scanner worker health + const lighthouseUrl = process.env.LIGHTHOUSE_SERVICE_URL || 'http://localhost:5001'; + const response = await fetch(`${lighthouseUrl}/health`); + + if (!response.ok) { + throw new Error(`Scanner worker health check failed: ${response.status}`); + } + + const health = await response.json(); + + return NextResponse.json({ + success: true, + scannerWorker: health, + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error('Health check error:', error); + return NextResponse.json( + { + error: 'Health check failed: ' + (error instanceof Error ? error.message : 'Unknown error'), + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/api/test-supabase/route.ts b/website-monitoring-frontend/src/app/api/test-supabase/route.ts new file mode 100644 index 0000000..3fa7323 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/test-supabase/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { supabase } from "@/lib/supabase"; + +export async function GET() { + try { + // Test basic database connection + const { data: websites, error: websitesError } = await supabase + .from('websites') + .select('count') + .limit(1); + + // Test scans table connection + const { data: scans, error: scansError } = await supabase + .from('scans') + .select('count') + .limit(1); + + // Test users table connection + const { data: users, error: usersError } = await supabase + .from('users') + .select('count') + .limit(1); + + return NextResponse.json({ + success: true, + database: { + websites: { + connected: !websitesError, + error: websitesError?.message || null + }, + scans: { + connected: !scansError, + error: scansError?.message || null + }, + users: { + connected: !usersError, + error: usersError?.message || null + } + }, + message: "Database connection test completed" + }); + } catch (error) { + console.error("Database test error:", error); + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : "Unknown error", + message: "Database connection test failed" + }, { status: 500 }); + } +} diff --git a/website-monitoring-frontend/src/app/api/test/route.ts b/website-monitoring-frontend/src/app/api/test/route.ts new file mode 100644 index 0000000..2cd2c17 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/test/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; +import { supabase } from "@/lib/supabase"; + +export async function GET() { + // Check auth state in API route + const { + data: { user }, + error: authError, + } = await supabase.auth.getUser(); + + console.log("API route auth check:", { + isAuthenticated: !!user, + userId: user?.id, + userEmail: user?.email, + authError: authError?.message, + }); + + return NextResponse.json({ + isAuthenticated: !!user, + userId: user?.id || null, + authError: authError?.message || null, + }); +} diff --git a/website-monitoring-frontend/src/app/api/validate-website/route.ts b/website-monitoring-frontend/src/app/api/validate-website/route.ts new file mode 100644 index 0000000..36be5d6 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/validate-website/route.ts @@ -0,0 +1,133 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; + +export async function POST(request: Request) { + try { + const { url } = await request.json(); + + if (!url) { + return NextResponse.json( + { error: "URL is required" }, + { status: 400 } + ); + } + + // Validate URL format + let validUrl: URL; + try { + validUrl = new URL(url); + } catch { + return NextResponse.json( + { error: "Invalid URL format" }, + { status: 400 } + ); + } + + // Only allow http and https protocols + if (!['http:', 'https:'].includes(validUrl.protocol)) { + return NextResponse.json( + { error: "Only HTTP and HTTPS URLs are supported" }, + { status: 400 } + ); + } + + try { + // Fetch the website with a timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout + + const response = await fetch(url, { + method: 'GET', + headers: { + 'User-Agent': 'CloudLense Website Validator/1.0', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + }, + signal: controller.signal, + redirect: 'follow', + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + return NextResponse.json({ + isValid: false, + error: `Website returned ${response.status} ${response.statusText}`, + }); + } + + // Get content type + const contentType = response.headers.get('content-type') || ''; + + if (!contentType.includes('text/html')) { + return NextResponse.json({ + isValid: false, + error: 'URL does not point to an HTML page', + }); + } + + // Parse HTML to extract metadata + const html = await response.text(); + + // Extract title + const titleMatch = html.match(/<title[^>]*>(.*?)<\/title>/i); + const title = titleMatch ? titleMatch[1].trim() : ''; + + // Extract meta description + const descriptionMatch = html.match(/<meta[^>]+name=['"]description['"][^>]+content=['"]([^'"]*)['"]/i); + const description = descriptionMatch ? descriptionMatch[1].trim() : ''; + + // Try to get favicon + const faviconMatch = html.match(/<link[^>]+rel=['"](?:icon|shortcut icon)['"][^>]+href=['"]([^'"]*)['"]/i); + let favicon = faviconMatch ? faviconMatch[1] : '/favicon.ico'; + + // Convert relative favicon URL to absolute + if (favicon && !favicon.startsWith('http')) { + favicon = new URL(favicon, url).href; + } + + // Validate favicon exists + let validFavicon: string | undefined; + try { + const faviconResponse = await fetch(favicon, { + method: 'HEAD', + signal: AbortSignal.timeout(5000), + }); + if (faviconResponse.ok) { + validFavicon = favicon; + } + } catch { + // If favicon fails, try the Google favicon service + validFavicon = `https://www.google.com/s2/favicons?domain=${validUrl.hostname}&sz=32`; + } + + return NextResponse.json({ + isValid: true, + title: title || validUrl.hostname, + description: description || '', + favicon: validFavicon, + hostname: validUrl.hostname, + protocol: validUrl.protocol, + }); + + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + return NextResponse.json({ + isValid: false, + error: 'Website took too long to respond (timeout)', + }); + } + + return NextResponse.json({ + isValid: false, + error: 'Unable to connect to website. Please check the URL and try again.', + }); + } + + } catch (error) { + console.error('Website validation error:', error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/website-monitoring-frontend/src/app/api/webhooks/website-change/route.ts b/website-monitoring-frontend/src/app/api/webhooks/website-change/route.ts new file mode 100644 index 0000000..720411c --- /dev/null +++ b/website-monitoring-frontend/src/app/api/webhooks/website-change/route.ts @@ -0,0 +1,242 @@ +import { NextResponse } from "next/server"; +import { lighthouseScanner } from "@/services/lighthouseScanner"; +import { logError } from "@/utils/errorUtils"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { websiteId, url, changeType, contentHash, metadata } = body; + + if (!websiteId || !url) { + return NextResponse.json( + { error: "Website ID and URL are required" }, + { status: 400 } + ); + } + + // Structured log for production visibility + console.info(JSON.stringify({ + level: 'info', + event: 'webhook_website_change_received', + websiteId, + changeType: changeType || 'unknown', + timestamp: new Date().toISOString(), + })); + + // Verify the webhook signature if needed + const signature = request.headers.get('x-webhook-signature'); + if (process.env.WEBHOOK_SECRET && signature) { + // Add webhook signature verification here if needed + // const isValid = verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET); + // if (!isValid) { + // return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); + // } + } + + // Get website details + const { getSupabaseAdmin } = await import("@/lib/admin"); + const supabase = getSupabaseAdmin(); + + const { data: website, error: websiteError } = await supabase + .from('websites') + .select(` + id, + organization_id, + organizations!inner ( + subscription_tier + ) + `) + .eq('id', websiteId) + .single(); + + if (websiteError || !website) { + return NextResponse.json( + { error: "Website not found" }, + { status: 404 } + ); + } + + // Check subscription limits + const { canScan, limits } = await lighthouseScanner.checkSubscriptionLimits( + String(website.organization_id) + ); + + if (!canScan) { + console.warn(JSON.stringify({ + level: 'warn', + event: 'subscription_limit_exceeded', + websiteId, + timestamp: new Date().toISOString(), + })); + return NextResponse.json( + { + success: false, + message: "Subscription limit exceeded - scan skipped", + limits, + }, + { status: 429 } + ); + } + + // Check if change detection is enabled for this subscription + const subscriptionLimits = getSubscriptionLimits(String((website as any).organizations?.subscription_tier || 'free')); + if (!subscriptionLimits.changeDetectionEnabled) { + console.warn(JSON.stringify({ + level: 'warn', + event: 'change_detection_disabled', + tier: String((website as any).organizations?.subscription_tier || 'unknown'), + websiteId, + timestamp: new Date().toISOString(), + })); + return NextResponse.json( + { + success: false, + message: "Change detection not available for this subscription tier", + }, + { status: 403 } + ); + } + + // Get or create the page record + let pageId: string; + const { data: existingPage } = await supabase + .from('pages') + .select('id, content_hash') + .eq('website_id', websiteId) + .eq('url', url) + .single(); + + if (existingPage) { + pageId = String((existingPage as any).id); + + // Update the page with new content hash + await supabase + .from('pages') + .update({ + content_hash: contentHash, + last_seen_at: new Date().toISOString(), + metadata: metadata || {}, + }) + .eq('id', pageId); + } else { + // Create new page record + const { data: newPage, error: createError } = await supabase + .from('pages') + .insert({ + website_id: websiteId, + url, + path: new URL(url).pathname, + content_hash: contentHash, + title: metadata?.title || 'Unknown Page', + is_active: true, + metadata: metadata || {}, + }) + .select('id') + .single(); + + if (createError || !newPage) { + throw new Error(`Failed to create page record: ${createError?.message}`); + } + + pageId = String((newPage as any).id); + } + + // Trigger a high-priority scan due to changes + const scanConfig = { + websiteId, + pageId, + deviceType: 'desktop' as const, + categories: ['performance', 'accessibility', 'seo', 'best_practices'] as ( + 'performance' | 'accessibility' | 'seo' | 'best_practices' + )[], + priority: 'high' as const, + triggeredBy: 'change_detection' as const, + }; + + const result = await lighthouseScanner.performScan(scanConfig); + + // Log the change detection + await supabase + .from('audit_logs') + .insert({ + website_id: websiteId, + action: 'change_detected', + entity_type: 'page', + entity_id: pageId, + changes: { + change_type: changeType || 'content_update', + url, + content_hash: contentHash, + metadata, + }, + }); + + if (result.success) { + console.info(JSON.stringify({ + level: 'info', + event: 'change_detection_scan_completed', + websiteId, + timestamp: new Date().toISOString(), + })); + return NextResponse.json({ + success: true, + scanId: result.scanId, + message: "Change detection scan triggered successfully", + metrics: result.metrics, + }); + } else { + console.error(JSON.stringify({ + level: 'error', + event: 'change_detection_scan_failed', + websiteId, + timestamp: new Date().toISOString(), + })); + return NextResponse.json( + { + success: false, + error: result.error, + }, + { status: 500 } + ); + } + } catch (error) { + const errorMsg = `Error processing website change webhook: ${error instanceof Error ? error.message : 'Unknown error'}`; + logError(errorMsg, error); + + return NextResponse.json( + { + success: false, + error: errorMsg, + }, + { status: 500 } + ); + } +} + +/** + * Get subscription limits based on tier + */ +function getSubscriptionLimits(tier: string) { + switch (tier) { + case 'free': + return { + changeDetectionEnabled: false, + }; + case 'starter': + return { + changeDetectionEnabled: true, + }; + case 'professional': + return { + changeDetectionEnabled: true, + }; + case 'enterprise': + return { + changeDetectionEnabled: true, + }; + default: + return { + changeDetectionEnabled: false, + }; + } +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/api/websites/[id]/settings/route.ts b/website-monitoring-frontend/src/app/api/websites/[id]/settings/route.ts new file mode 100644 index 0000000..87920d5 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/websites/[id]/settings/route.ts @@ -0,0 +1,255 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; +import { createClient } from "@supabase/supabase-js"; + +// Initialize Supabase with service role for admin operations + +export async function GET( + request: Request, + context: { params: Promise<{ id: string }> } +) { + try { + const url = new URL(request.url); + const userId = url.searchParams.get("userId"); + const { id: websiteId } = await context.params; + + if (!userId || !websiteId) { + return NextResponse.json({ + error: "Website ID and user ID are required" + }, { status: 400 }); + } + + // Get website to verify ownership + const { data: website, error: websiteError } = await getSupabaseAdmin() + .from("websites") + .select("organization_id, crawl_settings") + .eq("id", websiteId) + .single(); + + if (websiteError || !website) { + return NextResponse.json({ error: "Website not found" }, { status: 404 }); + } + + // Verify user has access to this website + const { data: userOrg, error: accessError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", userId) + .eq("organization_id", String((website as any).organization_id)) + .single(); + + if (accessError || !userOrg) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + // Get scan configurations + const { data: scanConfigs, error: scanError } = await getSupabaseAdmin() + .from("scan_configurations") + .select("*") + .eq("website_id", websiteId); + + // Get alert configurations + const { data: alertConfigs, error: alertError } = await getSupabaseAdmin() + .from("alert_configurations") + .select("*") + .eq("website_id", websiteId); + + if (scanError || alertError) { + console.error("Error fetching configurations:", { scanError, alertError }); + } + + // Structure the response + const crawl = (website as any).crawl_settings || {}; + const settings = { + scan: { + scanInterval: crawl.crawl_frequency === "hourly" ? 60 : + crawl.crawl_frequency === "daily" ? 1440 : 60, + maxPages: crawl.max_pages || 100, + maxDepth: crawl.max_depth || 3, + userAgent: crawl.user_agent || "", + excludePatterns: crawl.exclude_patterns || ["/admin/*", "/api/*"], + includePatterns: crawl.include_patterns || ["/*"], + respectRobotsTxt: crawl.respect_robots_txt !== false, + followRedirects: crawl.follow_redirects !== false, + maxConcurrentRequests: crawl.max_concurrent_requests || 3, + }, + alerts: { + performanceThreshold: 90, + seoThreshold: 90, + accessibilityThreshold: 90, + uptimeThreshold: 99, + notificationEmail: "", + slackWebhook: "", + enableEmailAlerts: true, + enableSlackAlerts: false, + alertFrequency: "immediate" as const, + }, + scanConfigurations: scanConfigs || [], + alertConfigurations: alertConfigs || [], + }; + + // Override with actual alert settings if they exist + if (alertConfigs && alertConfigs.length > 0) { + alertConfigs.forEach((config: any) => { + switch (config.metric) { + case "performance": + settings.alerts.performanceThreshold = Number(config.threshold) || settings.alerts.performanceThreshold; + break; + case "seo": + settings.alerts.seoThreshold = Number(config.threshold) || settings.alerts.seoThreshold; + break; + case "accessibility": + settings.alerts.accessibilityThreshold = Number(config.threshold) || settings.alerts.accessibilityThreshold; + break; + case "uptime": + settings.alerts.uptimeThreshold = Number(config.threshold) || settings.alerts.uptimeThreshold; + break; + } + settings.alerts.enableEmailAlerts = config.notification_channels?.includes("email") || false; + settings.alerts.enableSlackAlerts = config.notification_channels?.includes("slack") || false; + }); + } + + return NextResponse.json({ settings }); + } catch (error) { + console.error("Error in website settings GET:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function PUT( + request: Request, + context: { params: Promise<{ id: string }> } +) { + try { + const { userId, settingsType, settings } = await request.json(); + const { id: websiteId } = await context.params; + + if (!userId || !websiteId || !settingsType) { + return NextResponse.json({ + error: "Website ID, user ID, and settings type are required" + }, { status: 400 }); + } + + // Get website to verify ownership + const { data: website, error: websiteError } = await getSupabaseAdmin() + .from("websites") + .select("organization_id, crawl_settings") + .eq("id", websiteId) + .single(); + + if (websiteError || !website) { + return NextResponse.json({ error: "Website not found" }, { status: 404 }); + } + + // Verify user has permission to update this website + const { data: userOrg, error: accessError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", userId) + .eq("organization_id", String((website as any).organization_id)) + .single(); + + if (accessError || !userOrg) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + if (settingsType === "scan") { + // Update crawl settings + const updatedCrawlSettings = { + ...(website as any).crawl_settings || {}, + max_pages: settings.maxPages, + max_depth: settings.maxDepth, + user_agent: settings.userAgent, + exclude_patterns: settings.excludePatterns, + include_patterns: settings.includePatterns, + respect_robots_txt: settings.respectRobotsTxt, + follow_redirects: settings.followRedirects, + max_concurrent_requests: settings.maxConcurrentRequests, + crawl_frequency: settings.scanInterval === 60 ? "hourly" : + settings.scanInterval === 1440 ? "daily" : "hourly", + }; + + const { error: updateError } = await getSupabaseAdmin() + .from("websites") + .update({ crawl_settings: updatedCrawlSettings }) + .eq("id", websiteId); + + if (updateError) { + console.error("Error updating scan settings:", updateError); + return NextResponse.json({ error: "Failed to update scan settings" }, { status: 500 }); + } + + // Update or create scan configurations + const { error: scanConfigError } = await getSupabaseAdmin() + .from("scan_configurations") + .upsert([ + { + website_id: websiteId, + category: "performance", + interval_minutes: settings.scanInterval, + is_active: true, + priority: 1, + settings: { + lighthouse: true, + uptime: true, + max_pages: settings.maxPages, + max_depth: settings.maxDepth, + }, + }, + ]); + + if (scanConfigError) { + console.error("Error updating scan configuration:", scanConfigError); + } + + } else if (settingsType === "alerts") { + // Update alert configurations + const alertTypes = [ + { metric: "performance", threshold: settings.performanceThreshold }, + { metric: "seo", threshold: settings.seoThreshold }, + { metric: "accessibility", threshold: settings.accessibilityThreshold }, + { metric: "uptime", threshold: settings.uptimeThreshold }, + ]; + + const notificationChannels: string[] = []; + if (settings.enableEmailAlerts) notificationChannels.push("email"); + if (settings.enableSlackAlerts) notificationChannels.push("slack"); + + const alertConfigs = alertTypes.map(alert => ({ + website_id: websiteId, + metric: alert.metric, + threshold: alert.threshold, + comparison: "less_than", + notification_channels: notificationChannels, + is_active: true, + alert_frequency: settings.alertFrequency, + email_address: settings.notificationEmail || null, + slack_webhook: settings.slackWebhook || null, + })); + + // Delete existing configurations and insert new ones + await getSupabaseAdmin() + .from("alert_configurations") + .delete() + .eq("website_id", websiteId); + + const { error: alertError } = await getSupabaseAdmin() + .from("alert_configurations") + .insert(alertConfigs); + + if (alertError) { + console.error("Error updating alert configurations:", alertError); + return NextResponse.json({ error: "Failed to update alert settings" }, { status: 500 }); + } + } + + return NextResponse.json({ + success: true, + message: `${settingsType} settings updated successfully` + }); + } catch (error) { + console.error("Error in website settings PUT:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/api/websites/bulk/route.ts b/website-monitoring-frontend/src/app/api/websites/bulk/route.ts new file mode 100644 index 0000000..1764be9 --- /dev/null +++ b/website-monitoring-frontend/src/app/api/websites/bulk/route.ts @@ -0,0 +1,189 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; +import { createClient } from "@supabase/supabase-js"; + +// Initialize Supabase with service role for admin operations + +export async function POST(request: Request) { + try { + const { action, websiteIds, userId, updates } = await request.json(); + + if (!action || !websiteIds || !Array.isArray(websiteIds) || !userId) { + return NextResponse.json({ + error: "Action, website IDs array, and user ID are required" + }, { status: 400 }); + } + + // Verify user has access to all websites + const { data: websites, error: websitesError } = await getSupabaseAdmin() + .from("websites") + .select("id, organization_id, name") + .in("id", websiteIds); + + if (websitesError || !websites) { + return NextResponse.json({ error: "Failed to fetch websites" }, { status: 500 }); + } + + // Check if all websites belong to user's organization + const { data: userOrg, error: userError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", userId) + .single(); + + if (userError || !userOrg) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + const unauthorizedWebsites = websites.filter(w => w.organization_id !== userOrg.organization_id); + if (unauthorizedWebsites.length > 0) { + return NextResponse.json({ + error: "Access denied to some websites" + }, { status: 403 }); + } + + let results: any[] = []; + + switch (action) { + case "activate": + const { error: activateError } = await getSupabaseAdmin() + .from("websites") + .update({ is_active: true }) + .in("id", websiteIds); + + if (activateError) { + return NextResponse.json({ error: "Failed to activate websites" }, { status: 500 }); + } + + results = websites.map(w => ({ + id: w.id, + name: w.name, + status: "activated" + })); + break; + + case "deactivate": + const { error: deactivateError } = await getSupabaseAdmin() + .from("websites") + .update({ is_active: false }) + .in("id", websiteIds); + + if (deactivateError) { + return NextResponse.json({ error: "Failed to deactivate websites" }, { status: 500 }); + } + + results = websites.map(w => ({ + id: w.id, + name: w.name, + status: "deactivated" + })); + break; + + case "delete": + // Only owners and admins can delete websites + if (userOrg.role !== "owner" && userOrg.role !== "admin") { + return NextResponse.json({ + error: "Only owners and admins can delete websites" + }, { status: 403 }); + } + + const { error: deleteError } = await getSupabaseAdmin() + .from("websites") + .delete() + .in("id", websiteIds); + + if (deleteError) { + return NextResponse.json({ error: "Failed to delete websites" }, { status: 500 }); + } + + results = websites.map(w => ({ + id: w.id, + name: w.name, + status: "deleted" + })); + break; + + case "update": + if (!updates || typeof updates !== "object") { + return NextResponse.json({ + error: "Updates object is required for update action" + }, { status: 400 }); + } + + const updateData: any = {}; + if (updates.crawl_settings) updateData.crawl_settings = updates.crawl_settings; + if (updates.is_active !== undefined) updateData.is_active = updates.is_active; + + const { error: updateError } = await getSupabaseAdmin() + .from("websites") + .update(updateData) + .in("id", websiteIds); + + if (updateError) { + return NextResponse.json({ error: "Failed to update websites" }, { status: 500 }); + } + + results = websites.map(w => ({ + id: w.id, + name: w.name, + status: "updated" + })); + break; + + case "scan": + // Trigger scans for all websites + const scanPromises = websiteIds.map(async (websiteId: string) => { + try { + // Insert scan request + const { error: scanError } = await getSupabaseAdmin() + .from("scans") + .insert([ + { + website_id: websiteId, + status: "pending", + triggered_by: userId, + scan_type: "manual", + } + ]); + + if (scanError) { + console.error(`Failed to trigger scan for website ${websiteId}:`, scanError); + return { id: websiteId, status: "scan_failed", error: scanError.message }; + } + + return { id: websiteId, status: "scan_triggered" }; + } catch (error) { + console.error(`Error triggering scan for website ${websiteId}:`, error); + return { id: websiteId, status: "scan_failed" }; + } + }); + + const scanResults = await Promise.all(scanPromises); + results = websites.map(w => { + const scanResult = scanResults.find(r => r.id === w.id); + return { + id: w.id, + name: w.name, + status: scanResult?.status || "scan_failed", + }; + }); + break; + + default: + return NextResponse.json({ + error: "Invalid action. Supported actions: activate, deactivate, delete, update, scan" + }, { status: 400 }); + } + + return NextResponse.json({ + success: true, + action, + results, + message: `Successfully ${action}d ${results.length} website(s)` + }); + + } catch (error) { + console.error("Error in websites bulk operation:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/api/websites/route.ts b/website-monitoring-frontend/src/app/api/websites/route.ts new file mode 100644 index 0000000..c21b56c --- /dev/null +++ b/website-monitoring-frontend/src/app/api/websites/route.ts @@ -0,0 +1,297 @@ +import { NextResponse } from "next/server"; +import { getSupabaseAdmin } from "@/lib/admin"; +import { createClient } from "@supabase/supabase-js"; + +// Initialize Supabase with service role for admin operations + +export async function GET(request: Request) { + try { + const url = new URL(request.url); + const organizationId = url.searchParams.get("organizationId"); + const userId = url.searchParams.get("userId"); + + if (!organizationId || !userId) { + return NextResponse.json({ + error: "Organization ID and User ID are required" + }, { status: 400 }); + } + + // Verify user has access to this organization + const { data: userOrg, error: accessError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", userId) + .eq("organization_id", organizationId) + .single(); + + if (accessError || !userOrg) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + // Get websites with their latest scan information + const { data: websites, error: websitesError } = await getSupabaseAdmin() + .from("websites") + .select(` + id, + name, + base_url, + is_active, + created_at, + settings + `) + .eq("organization_id", organizationId) + .order("created_at", { ascending: false }); + + if (websitesError) { + console.error("Error fetching websites:", websitesError); + return NextResponse.json({ error: "Failed to fetch websites" }, { status: 500 }); + } + + // Get additional stats for each website + const websiteIds = websites?.map(w => w.id) || []; + + if (websiteIds.length > 0) { + const [pagesData, scansData] = await Promise.all([ + getSupabaseAdmin() + .from("pages") + .select("website_id") + .in("website_id", websiteIds), + + getSupabaseAdmin() + .from("scans") + .select("website_id, status, lighthouse_score") + .in("website_id", websiteIds) + .order("created_at", { ascending: false }) + ]); + + // Count pages per website + const pagesCounts = (pagesData.data as Array<{ website_id: string }> | null | undefined)?.reduce((acc: Record<string, number>, page) => { + const key = String(page.website_id); + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {} as Record<string, number>) || {}; + + // Get latest scan per website + const latestScans = (scansData.data as Array<{ website_id: string }> | null | undefined)?.reduce((acc: Record<string, any>, scan: any) => { + const key = String(scan.website_id); + if (!acc[key]) { + acc[key] = scan; + } + return acc; + }, {} as Record<string, any>) || {}; + + // Add stats to websites + const websitesWithStats = (websites as Array<any>).map((website: any) => ({ + ...website, + stats: { + pagesCount: pagesCounts[String(website.id)] || 0, + latestScan: latestScans[String(website.id)] || null, + } + })); + + return NextResponse.json({ websites: websitesWithStats }); + } + + return NextResponse.json({ websites: websites || [] }); + } catch (error) { + console.error("Error in websites GET:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function POST(request: Request) { + try { + const { name, base_url, organizationId, userId, crawl_settings } = await request.json(); + + if (!name || !base_url || !organizationId || !userId) { + return NextResponse.json({ + error: "Name, URL, organization ID, and user ID are required" + }, { status: 400 }); + } + + // Verify user has permission to add websites to this organization + const { data: userOrg, error: accessError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", userId) + .eq("organization_id", organizationId) + .single(); + + if (accessError || !userOrg) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + // Normalize URL + let normalizedUrl = base_url; + if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) { + normalizedUrl = `https://${normalizedUrl}`; + } + normalizedUrl = normalizedUrl.replace(/\/+$/, ""); + + // Default crawl settings + const defaultCrawlSettings = { + max_depth: 3, + max_pages: 100, + exclude_patterns: ["/admin/*", "/api/*", "*.pdf", "*.jpg", "*.png"], + include_patterns: ["/*"], + respect_robots_txt: true, + crawl_frequency: "daily", + ...crawl_settings + }; + + // Create website + const { data: website, error: createError } = await getSupabaseAdmin() + .from("websites") + .insert([ + { + name, + base_url: normalizedUrl, + organization_id: organizationId, + is_active: true, + crawl_settings: defaultCrawlSettings, + scan_status: "pending" + }, + ]) + .select() + .single(); + + if (createError) { + console.error("Error creating website:", createError); + return NextResponse.json({ error: "Failed to create website" }, { status: 500 }); + } + + return NextResponse.json({ website }); + } catch (error) { + console.error("Error in websites POST:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function PUT(request: Request) { + try { + const { id, name, base_url, is_active, crawl_settings, userId } = await request.json(); + + if (!id || !userId) { + return NextResponse.json({ + error: "Website ID and user ID are required" + }, { status: 400 }); + } + + // Get website to verify ownership + const { data: website, error: websiteError } = await getSupabaseAdmin() + .from("websites") + .select("organization_id") + .eq("id", id) + .single(); + + if (websiteError || !website) { + return NextResponse.json({ error: "Website not found" }, { status: 404 }); + } + + // Verify user has permission to update this website + const { data: userOrg, error: accessError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", userId) + .eq("organization_id", String((website as any).organization_id)) + .single(); + + if (accessError || !userOrg) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + // Build update object + const updateData: any = {}; + + if (name !== undefined) updateData.name = name; + if (base_url !== undefined) { + let normalizedUrl = base_url; + if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) { + normalizedUrl = `https://${normalizedUrl}`; + } + updateData.base_url = normalizedUrl.replace(/\/+$/, ""); + } + if (is_active !== undefined) updateData.is_active = is_active; + if (crawl_settings !== undefined) updateData.crawl_settings = crawl_settings; + + // Update website + const { data: updatedWebsite, error: updateError } = await getSupabaseAdmin() + .from("websites") + .update(updateData) + .eq("id", id) + .select() + .single(); + + if (updateError) { + console.error("Error updating website:", updateError); + return NextResponse.json({ error: "Failed to update website" }, { status: 500 }); + } + + return NextResponse.json({ website: updatedWebsite }); + } catch (error) { + console.error("Error in websites PUT:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function DELETE(request: Request) { + try { + const url = new URL(request.url); + const id = url.searchParams.get("id"); + const userId = url.searchParams.get("userId"); + + if (!id || !userId) { + return NextResponse.json({ + error: "Website ID and user ID are required" + }, { status: 400 }); + } + + // Get website to verify ownership + const { data: website, error: websiteError } = await getSupabaseAdmin() + .from("websites") + .select("organization_id, name") + .eq("id", id) + .single(); + + if (websiteError || !website) { + return NextResponse.json({ error: "Website not found" }, { status: 404 }); + } + + // Verify user has permission to delete this website + const { data: userOrg, error: accessError } = await getSupabaseAdmin() + .from("users") + .select("organization_id, role") + .eq("id", userId) + .eq("organization_id", String((website as any).organization_id)) + .single(); + + if (accessError || !userOrg) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + if (userOrg.role !== "owner" && userOrg.role !== "admin") { + return NextResponse.json({ + error: "Only owners and admins can delete websites" + }, { status: 403 }); + } + + // Delete website (CASCADE should handle related records) + const { error: deleteError } = await getSupabaseAdmin() + .from("websites") + .delete() + .eq("id", id); + + if (deleteError) { + console.error("Error deleting website:", deleteError); + return NextResponse.json({ error: "Failed to delete website" }, { status: 500 }); + } + + return NextResponse.json({ + success: true, + message: `Website "${website.name}" deleted successfully` + }); + } catch (error) { + console.error("Error in websites DELETE:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/auth/callback/page.tsx b/website-monitoring-frontend/src/app/auth/callback/page.tsx new file mode 100644 index 0000000..3ec28db --- /dev/null +++ b/website-monitoring-frontend/src/app/auth/callback/page.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { supabase } from "@/lib/supabase"; + +export default function AuthCallbackPage() { + const router = useRouter(); + + useEffect(() => { + const handleAuthCallback = async () => { + try { + const { searchParams } = new URL(window.location.href); + const token = searchParams.get("token"); + const type = searchParams.get("type"); + + if (token && type === "email_verification") { + const { error } = await supabase.auth.verifyOtp({ + token_hash: token, + type: "email", + }); + + if (error) { + throw error; + } + + // Redirect to dashboard after successful verification + router.push("/dashboard"); + } + } catch (error) { + console.error("Error during auth callback:", error); + router.push("/auth?error=verification_failed"); + } + }; + + handleAuthCallback(); + }, [router]); + + return ( + <div className="min-h-screen flex items-center justify-center bg-gradient-to-r from-blue-500 to-purple-500"> + <div className="bg-white p-8 rounded-lg shadow-xl text-center"> + <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div> + <h2 className="text-xl font-semibold text-gray-900 mb-2"> + Verifying your email... + </h2> + <p className="text-gray-600"> + Please wait while we complete the process. + </p> + </div> + </div> + ); +} diff --git a/website-monitoring-frontend/src/app/auth/fix-account/page.tsx b/website-monitoring-frontend/src/app/auth/fix-account/page.tsx new file mode 100644 index 0000000..ef851e0 --- /dev/null +++ b/website-monitoring-frontend/src/app/auth/fix-account/page.tsx @@ -0,0 +1,9 @@ +import { FixAccount } from "@/components/auth/FixAccount"; + +export default function FixAccountPage() { + return ( + <div className="min-h-screen flex items-center justify-center bg-gray-50 p-4"> + <FixAccount /> + </div> + ); +} diff --git a/website-monitoring-frontend/src/app/auth/page.tsx b/website-monitoring-frontend/src/app/auth/page.tsx new file mode 100644 index 0000000..b9eacc3 --- /dev/null +++ b/website-monitoring-frontend/src/app/auth/page.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { AuthForm } from "@/components/auth/AuthForm"; +import { Shield, ArrowLeft } from "lucide-react"; +import { useSearchParams } from "next/navigation"; +import { motion } from "framer-motion"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/contexts/AuthContext"; + +export default function AuthPage() { + const searchParams = useSearchParams(); + const source = searchParams.get("source"); + const email = searchParams.get("email"); + const [mounted, setMounted] = useState(false); + const router = useRouter(); + const { user } = useAuth(); + + useEffect(() => { + setMounted(true); + if (user) { + router.push("/dashboard"); + } + }, [user, router]); + + return ( + <div className="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 to-white p-4"> + {/* Animated background elements */} + <div className="absolute inset-0 overflow-hidden"> + <div className="absolute inset-0 bg-grid-pattern opacity-[0.02]" /> + + {mounted && ( + <> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 0.5 }} + transition={{ duration: 1 }} + className="absolute top-0 -left-4 w-[500px] h-[500px] bg-gradient-to-br from-blue-400/20 to-indigo-400/20 rounded-full blur-3xl" + style={{ filter: "blur(120px)" }} + /> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 0.5 }} + transition={{ duration: 1, delay: 0.2 }} + className="absolute bottom-0 -right-4 w-[500px] h-[500px] bg-gradient-to-br from-purple-400/20 to-pink-400/20 rounded-full blur-3xl" + style={{ filter: "blur(120px)" }} + /> + </> + )} + </div> + + {/* Main content container */} + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.5, delay: 0.1 }} + className="relative w-full max-w-lg mx-auto" + > + {/* Logo and name above the card */} + <div className="absolute top-[-40px] left-1/2 transform -translate-x-1/2 flex items-center space-x-2"> + <Shield className="h-8 w-8 text-blue-600" /> + <span className="text-2xl font-semibold bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-indigo-600"> + CloudLense + </span> + </div> + + {/* Glass card effect */} + <div className="relative backdrop-blur-xl bg-white/80 rounded-2xl shadow-xl border border-white/20 overflow-hidden"> + {/* Card header */} + <div className="px-8 pt-8"> + <motion.div + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.5, delay: 0.2 }} + className="flex items-center justify-between" + > + <div + className="flex items-center space-x-4 cursor-pointer transition-transform transform hover:scale-110" + onClick={() => router.push("/")} + > + <div className="flex items-center space-x-2"> + <ArrowLeft className="h-4 w-4 text-blue-600" /> + <span className="text-sm font-medium text-blue-600"> + Back to Home + </span> + </div> + </div> + </motion.div> + + <motion.div + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.5, delay: 0.3 }} + className="mt-6 mb-4" + > + <h1 className="text-2xl font-bold text-gray-900"> + {source === "hero" ? "Get Started" : "Welcome"} + </h1> + <p className="mt-2 text-gray-600"> + {source === "hero" + ? "Set up your account in seconds" + : "Sign in to continue to your dashboard"} + </p> + </motion.div> + </div> + + {/* Form section */} + <div className="p-8"> + <AuthForm initialEmail={email} /> + </div> + + {/* Footer */} + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + transition={{ duration: 0.5, delay: 0.4 }} + className="px-8 pb-8 text-center" + > + <p className="text-sm text-gray-500"> + By continuing, you agree to our{" "} + <a href="#" className="text-blue-600 hover:underline"> + Terms + </a>{" "} + and{" "} + <a href="#" className="text-blue-600 hover:underline"> + Privacy Policy + </a> + </p> + </motion.div> + </div> + + {/* Decorative elements */} + <motion.div + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ duration: 0.5, delay: 0.5 }} + className="absolute -z-10 inset-0 border border-blue-100 rounded-2xl -m-2" + /> + <motion.div + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ duration: 0.5, delay: 0.6 }} + className="absolute -z-10 inset-0 border border-blue-50 rounded-2xl -m-4" + /> + </motion.div> + </div> + ); +} diff --git a/website-monitoring-frontend/src/app/auth/reset-password/page.tsx b/website-monitoring-frontend/src/app/auth/reset-password/page.tsx new file mode 100644 index 0000000..e616584 --- /dev/null +++ b/website-monitoring-frontend/src/app/auth/reset-password/page.tsx @@ -0,0 +1,281 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { motion } from "framer-motion"; +import { supabase } from "@/lib/supabase"; +import { Button } from "@/components/ui/forms/Button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/forms/Form"; +import { Input } from "@/components/ui/forms/Input"; +import { Shield, Key, Loader2, Check, ArrowLeft } from "lucide-react"; +import { ErrorFeedback } from "@/components/ui/ErrorFeedback"; + +const resetPasswordSchema = z.object({ + password: z.string().min(6, "Password must be at least 6 characters"), + confirmPassword: z.string().min(6, "Password must be at least 6 characters"), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], +}); + +export default function ResetPasswordPage() { + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(""); + const [mounted, setMounted] = useState(false); + const router = useRouter(); + const searchParams = useSearchParams(); + + const form = useForm<z.infer<typeof resetPasswordSchema>>({ + resolver: zodResolver(resetPasswordSchema), + defaultValues: { password: "", confirmPassword: "" }, + }); + + useEffect(() => { + setMounted(true); + + // Check if we have the proper reset token in the URL + const accessToken = searchParams.get('access_token'); + const refreshToken = searchParams.get('refresh_token'); + + if (!accessToken || !refreshToken) { + setError("Invalid or expired reset link. Please request a new password reset."); + return; + } + + // Set the session with the tokens from the URL + supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }); + }, [searchParams]); + + const handlePasswordReset = async (data: z.infer<typeof resetPasswordSchema>) => { + try { + setLoading(true); + setError(""); + + const { error } = await supabase.auth.updateUser({ + password: data.password, + }); + + if (error) { + throw error; + } + + setSuccess(true); + + // Redirect to dashboard after successful reset + setTimeout(() => { + router.push("/dashboard"); + }, 2000); + } catch (error) { + console.error("Password reset error:", error); + if (error instanceof Error) { + setError(error.message); + } else { + setError("Failed to reset password. Please try again."); + } + } finally { + setLoading(false); + } + }; + + if (!mounted) return null; + + return ( + <div className="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 to-white p-4"> + {/* Animated background elements */} + <div className="absolute inset-0 overflow-hidden"> + <div className="absolute inset-0 bg-grid-pattern opacity-[0.02]" /> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 0.5 }} + transition={{ duration: 1 }} + className="absolute top-0 -left-4 w-[500px] h-[500px] bg-gradient-to-br from-blue-400/20 to-indigo-400/20 rounded-full blur-3xl" + style={{ filter: "blur(120px)" }} + /> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 0.5 }} + transition={{ duration: 1, delay: 0.2 }} + className="absolute bottom-0 -right-4 w-[500px] h-[500px] bg-gradient-to-br from-purple-400/20 to-pink-400/20 rounded-full blur-3xl" + style={{ filter: "blur(120px)" }} + /> + </div> + + {/* Main content container */} + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.5, delay: 0.1 }} + className="relative w-full max-w-lg mx-auto" + > + {/* Logo and name above the card */} + <div className="absolute top-[-40px] left-1/2 transform -translate-x-1/2 flex items-center space-x-2"> + <Shield className="h-8 w-8 text-blue-600" /> + <span className="text-2xl font-semibold bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-indigo-600"> + CloudLense + </span> + </div> + + {/* Glass card effect */} + <div className="relative backdrop-blur-xl bg-white/80 rounded-2xl shadow-xl border border-white/20 overflow-hidden"> + {/* Card header */} + <div className="px-8 pt-8"> + <motion.div + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.5, delay: 0.2 }} + className="flex items-center justify-between" + > + <div + className="flex items-center space-x-4 cursor-pointer transition-transform transform hover:scale-110" + onClick={() => router.push("/auth")} + > + <div className="flex items-center space-x-2"> + <ArrowLeft className="h-4 w-4 text-blue-600" /> + <span className="text-sm font-medium text-blue-600"> + Back to Login + </span> + </div> + </div> + </motion.div> + + <motion.div + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.5, delay: 0.3 }} + className="mt-6 mb-4" + > + <h1 className="text-2xl font-bold text-gray-900"> + Reset Your Password + </h1> + <p className="mt-2 text-gray-600"> + Enter your new password below + </p> + </motion.div> + </div> + + {/* Form section */} + <div className="p-8"> + {success ? ( + <motion.div + initial={{ opacity: 0, scale: 0.95 }} + animate={{ opacity: 1, scale: 1 }} + className="text-center space-y-4" + > + <div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto"> + <Check className="w-8 h-8 text-green-600" /> + </div> + <div> + <h2 className="text-xl font-semibold text-gray-900">Password Reset Successfully</h2> + <p className="text-gray-600 mt-2"> + Your password has been updated. Redirecting to dashboard... + </p> + </div> + </motion.div> + ) : ( + <Form {...form}> + <form + onSubmit={form.handleSubmit(handlePasswordReset)} + className="space-y-4" + > + <FormField + control={form.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormControl> + <div className="relative"> + <Key className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" /> + <Input + {...field} + type="password" + placeholder="Enter new password" + className="pl-10" + disabled={loading} + /> + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="confirmPassword" + render={({ field }) => ( + <FormItem> + <FormControl> + <div className="relative"> + <Key className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" /> + <Input + {...field} + type="password" + placeholder="Confirm new password" + className="pl-10" + disabled={loading} + /> + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <Button + type="submit" + disabled={loading} + className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 transition-all duration-300" + > + {loading ? ( + <Loader2 className="h-5 w-5 animate-spin" /> + ) : ( + "Update Password" + )} + </Button> + </form> + </Form> + )} + + {/* Error Message */} + {error && ( + <div className="mt-4"> + <ErrorFeedback + title="Password Reset Failed" + message={error} + details="Please try requesting a new password reset link." + severity="error" + onDismiss={() => setError("")} + /> + </div> + )} + </div> + </div> + + {/* Decorative elements */} + <motion.div + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ duration: 0.5, delay: 0.5 }} + className="absolute -z-10 inset-0 border border-blue-100 rounded-2xl -m-2" + /> + <motion.div + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ duration: 0.5, delay: 0.6 }} + className="absolute -z-10 inset-0 border border-blue-50 rounded-2xl -m-4" + /> + </motion.div> + </div> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/dashboard/alerts/page.tsx b/website-monitoring-frontend/src/app/dashboard/alerts/page.tsx new file mode 100644 index 0000000..efd36ed --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/alerts/page.tsx @@ -0,0 +1,494 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Bell, + AlertTriangle, + CheckCircle, + XCircle, + Mail, + Smartphone, + Settings, + Clock, + TrendingDown, + Zap, + Plus, + Loader2, +} from "lucide-react"; +import { useAuth } from "@/contexts/AuthContext"; +import { supabase } from "@/lib/supabase"; +import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils"; + +interface Alert { + id: string; + type: "downtime" | "performance" | "error" | "ssl" | "maintenance"; + severity: "low" | "medium" | "high" | "critical"; + title: string; + message: string; + website_name: string; + website_url: string; + status: "active" | "resolved" | "acknowledged"; + created_at: string; + resolved_at?: string; + acknowledged_at?: string; +} + +interface AlertRule { + id: string; + name: string; + type: "downtime" | "performance" | "error_rate"; + condition: string; + threshold: number; + enabled: boolean; + notification_methods: string[]; + created_at: string; +} + +export default function AlertsPage() { + const { userDetails } = useAuth(); + const [alerts, setAlerts] = useState<Alert[]>([]); + const [alertRules, setAlertRules] = useState<AlertRule[]>([]); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState<"alerts" | "rules">("alerts"); + const [processingAlert, setProcessingAlert] = useState<string | null>(null); + + useEffect(() => { + if (userDetails?.organization_id) { + loadAlertsData(); + } + }, [userDetails]); + + const loadAlertsData = async () => { + if (!userDetails?.organization_id) return; + + try { + setLoading(true); + + // Load alerts + const { data: alertsData, error: alertsError } = await supabase + .from("alerts") + .select(` + id, + type, + severity, + title, + message, + status, + created_at, + resolved_at, + acknowledged_at, + websites!inner ( + name, + base_url, + organization_id + ) + `) + .eq("websites.organization_id", userDetails.organization_id) + .order("created_at", { ascending: false }) + .limit(50); + + if (alertsError) throw alertsError; + + const formattedAlerts: Alert[] = alertsData?.map((alert: any) => ({ + id: alert.id, + type: alert.type, + severity: alert.severity, + title: alert.title, + message: alert.message, + website_name: alert.websites.name, + website_url: alert.websites.base_url, + status: alert.status, + created_at: alert.created_at, + resolved_at: alert.resolved_at, + acknowledged_at: alert.acknowledged_at, + })) || []; + + setAlerts(formattedAlerts); + + // Load alert rules + const { data: rulesData, error: rulesError } = await supabase + .from("alert_rules") + .select("*") + .eq("organization_id", userDetails.organization_id) + .order("created_at", { ascending: false }); + + if (rulesError) throw rulesError; + setAlertRules(rulesData || []); + + } catch (error) { + const errorInfo = extractSupabaseErrorInfo(error); + logError("Error loading alerts data", error, { + organizationId: userDetails.organization_id, + function: "loadAlertsData", + supabaseError: errorInfo + }); + } finally { + setLoading(false); + } + }; + + const handleAlertAction = async (alertId: string, action: "acknowledge" | "resolve") => { + try { + setProcessingAlert(alertId); + + const updateData = action === "acknowledge" + ? { status: "acknowledged" as const, acknowledged_at: new Date().toISOString() } + : { status: "resolved" as const, resolved_at: new Date().toISOString() }; + + const { error } = await supabase + .from("alerts") + .update(updateData) + .eq("id", alertId); + + if (error) throw error; + + // Update local state + setAlerts(prev => prev.map(alert => + alert.id === alertId + ? { ...alert, ...updateData } + : alert + )); + + } catch (error) { + console.error(`Error ${action}ing alert:`, error); + } finally { + setProcessingAlert(null); + } + }; + + const toggleAlertRule = async (ruleId: string, enabled: boolean) => { + try { + const { error } = await supabase + .from("alert_rules") + .update({ enabled: !enabled }) + .eq("id", ruleId); + + if (error) throw error; + + setAlertRules(prev => prev.map(rule => + rule.id === ruleId + ? { ...rule, enabled: !enabled } + : rule + )); + + } catch (error) { + console.error("Error toggling alert rule:", error); + } + }; + + const getAlertIcon = (type: string, severity: string) => { + const iconClass = severity === "critical" + ? "text-red-500" + : severity === "high" + ? "text-orange-500" + : severity === "medium" + ? "text-yellow-500" + : "text-blue-500"; + + switch (type) { + case "downtime": + return <XCircle className={`w-4 h-4 ${iconClass}`} />; + case "performance": + return <TrendingDown className={`w-4 h-4 ${iconClass}`} />; + case "error": + return <AlertTriangle className={`w-4 h-4 ${iconClass}`} />; + case "ssl": + return <Settings className={`w-4 h-4 ${iconClass}`} />; + default: + return <Clock className={`w-4 h-4 ${iconClass}`} />; + } + }; + + const getSeverityColor = (severity: string) => { + switch (severity) { + case "critical": + return "bg-red-100 text-red-800"; + case "high": + return "bg-orange-100 text-orange-800"; + case "medium": + return "bg-yellow-100 text-yellow-800"; + default: + return "bg-blue-100 text-blue-800"; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "resolved": + return "bg-green-100 text-green-800"; + case "acknowledged": + return "bg-blue-100 text-blue-800"; + default: + return "bg-red-100 text-red-800"; + } + }; + + if (loading) { + return ( + <DashboardLayout> + <div className="flex items-center justify-center py-12"> + <Loader2 className="w-8 h-8 animate-spin text-blue-600" /> + </div> + </DashboardLayout> + ); + } + + const activeAlerts = alerts.filter(a => a.status === "active").length; + const acknowledgedAlerts = alerts.filter(a => a.status === "acknowledged").length; + const resolvedAlerts = alerts.filter(a => a.status === "resolved").length; + const criticalAlerts = alerts.filter(a => a.severity === "critical" && a.status === "active").length; + + return ( + <DashboardLayout> + <div className="space-y-6"> + {/* Header */} + <div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between"> + <div> + <h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2"> + <Bell className="w-6 h-6" /> + Alerts & Notifications + </h1> + <p className="text-gray-600 mt-1"> + Monitor and manage alerts for your websites + </p> + </div> + <div className="flex gap-2"> + <Button + variant={activeTab === "alerts" ? "default" : "outline"} + onClick={() => setActiveTab("alerts")} + className="flex items-center gap-2" + > + <Bell className="w-4 h-4" /> + Alerts + </Button> + <Button + variant={activeTab === "rules" ? "default" : "outline"} + onClick={() => setActiveTab("rules")} + className="flex items-center gap-2" + > + <Settings className="w-4 h-4" /> + Rules + </Button> + </div> + </div> + + {/* Summary Cards */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Active Alerts</p> + <p className="text-2xl font-bold text-red-600">{activeAlerts}</p> + </div> + <AlertTriangle className="w-8 h-8 text-red-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Critical</p> + <p className="text-2xl font-bold text-orange-600">{criticalAlerts}</p> + </div> + <XCircle className="w-8 h-8 text-orange-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Acknowledged</p> + <p className="text-2xl font-bold text-blue-600">{acknowledgedAlerts}</p> + </div> + <Clock className="w-8 h-8 text-blue-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Resolved</p> + <p className="text-2xl font-bold text-green-600">{resolvedAlerts}</p> + </div> + <CheckCircle className="w-8 h-8 text-green-600" /> + </div> + </CardContent> + </Card> + </div> + + {/* Content */} + {activeTab === "alerts" ? ( + <div className="space-y-4"> + <h2 className="text-lg font-semibold text-gray-900">Recent Alerts</h2> + + {alerts.length > 0 ? ( + <div className="space-y-4"> + {alerts.map((alert) => ( + <Card key={alert.id}> + <CardContent className="p-6"> + <div className="flex items-start justify-between"> + <div className="flex items-start gap-4"> + {getAlertIcon(alert.type, alert.severity)} + <div className="flex-1"> + <div className="flex items-center gap-2 mb-1"> + <h3 className="font-semibold text-gray-900">{alert.title}</h3> + <Badge className={getSeverityColor(alert.severity)}> + {alert.severity.toUpperCase()} + </Badge> + <Badge className={getStatusColor(alert.status)}> + {alert.status.toUpperCase()} + </Badge> + </div> + <p className="text-sm text-gray-600 mb-2">{alert.message}</p> + <div className="flex items-center gap-4 text-xs text-gray-500"> + <span>{alert.website_name}</span> + <span>•</span> + <span>{new Date(alert.created_at).toLocaleString()}</span> + {alert.resolved_at && ( + <> + <span>•</span> + <span>Resolved: {new Date(alert.resolved_at).toLocaleString()}</span> + </> + )} + </div> + </div> + </div> + + {alert.status === "active" && ( + <div className="flex gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => handleAlertAction(alert.id, "acknowledge")} + disabled={processingAlert === alert.id} + className="flex items-center gap-1" + > + {processingAlert === alert.id ? ( + <Loader2 className="w-3 h-3 animate-spin" /> + ) : ( + <Clock className="w-3 h-3" /> + )} + Acknowledge + </Button> + <Button + variant="outline" + size="sm" + onClick={() => handleAlertAction(alert.id, "resolve")} + disabled={processingAlert === alert.id} + className="flex items-center gap-1 text-green-600 hover:text-green-700" + > + {processingAlert === alert.id ? ( + <Loader2 className="w-3 h-3 animate-spin" /> + ) : ( + <CheckCircle className="w-3 h-3" /> + )} + Resolve + </Button> + </div> + )} + </div> + </CardContent> + </Card> + ))} + </div> + ) : ( + <Card> + <CardContent className="p-12 text-center"> + <Bell className="w-16 h-16 mx-auto text-gray-400 mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + No alerts found + </h3> + <p className="text-gray-600"> + When issues are detected with your websites, alerts will appear here + </p> + </CardContent> + </Card> + )} + </div> + ) : ( + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <h2 className="text-lg font-semibold text-gray-900">Alert Rules</h2> + <Button className="flex items-center gap-2"> + <Plus className="w-4 h-4" /> + Create Rule + </Button> + </div> + + {alertRules.length > 0 ? ( + <div className="space-y-4"> + {alertRules.map((rule) => ( + <Card key={rule.id}> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <h3 className="font-semibold text-gray-900">{rule.name}</h3> + <p className="text-sm text-gray-600"> + {rule.type} {rule.condition} {rule.threshold} + </p> + <div className="flex items-center gap-2 mt-2"> + {rule.notification_methods.includes("email") && ( + <Badge variant="outline" className="flex items-center gap-1"> + <Mail className="w-3 h-3" /> + Email + </Badge> + )} + {rule.notification_methods.includes("sms") && ( + <Badge variant="outline" className="flex items-center gap-1"> + <Smartphone className="w-3 h-3" /> + SMS + </Badge> + )} + </div> + </div> + <div className="flex items-center gap-4"> + <Badge className={rule.enabled ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}> + {rule.enabled ? "Enabled" : "Disabled"} + </Badge> + <Button + variant="outline" + size="sm" + onClick={() => toggleAlertRule(rule.id, rule.enabled)} + > + {rule.enabled ? "Disable" : "Enable"} + </Button> + </div> + </div> + </CardContent> + </Card> + ))} + </div> + ) : ( + <Card> + <CardContent className="p-12 text-center"> + <Settings className="w-16 h-16 mx-auto text-gray-400 mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + No alert rules configured + </h3> + <p className="text-gray-600 mb-6"> + Create alert rules to get notified about website issues + </p> + <Button className="flex items-center gap-2"> + <Plus className="w-4 h-4" /> + Create Your First Rule + </Button> + </CardContent> + </Card> + )} + </div> + )} + </div> + </DashboardLayout> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/dashboard/diagnostics/page.tsx b/website-monitoring-frontend/src/app/dashboard/diagnostics/page.tsx new file mode 100644 index 0000000..1439b87 --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/diagnostics/page.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { supabase } from "@/lib/supabase"; +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { useAuth } from "@/contexts/AuthContext"; + +export default function DiagnosticsPage() { + const [results, setResults] = useState<any>({}); + const [isRunning, setIsRunning] = useState(false); + const { user, userDetails } = useAuth(); + + const runDiagnostics = async () => { + setIsRunning(true); + const diagnosticResults: any = { + timestamp: new Date().toISOString(), + auth: { user, userDetails }, + }; + + try { + // Test general permissions + const { data: authTest, error: authError } = + await supabase.auth.getUser(); + diagnosticResults.authTest = { data: authTest, error: authError }; + + // Test websites table access - select + const { data: selectTest, error: selectError } = await supabase + .from("websites") + .select("*") + .limit(5); + diagnosticResults.selectTest = { data: selectTest, error: selectError }; + + // Test organizations table access + const { data: orgTest, error: orgError } = await supabase + .from("organizations") + .select("*") + .limit(5); + diagnosticResults.orgTest = { data: orgTest, error: orgError }; + + // Test insert (with immediate deletion to avoid clutter) + const testName = `Test Website ${new Date().toISOString()}`; + const { data: insertTest, error: insertError } = await supabase + .from("websites") + .insert([ + { + name: testName, + base_url: "https://example.com/test", + organization_id: userDetails?.organization_id, + is_active: true, + }, + ]) + .select(); + diagnosticResults.insertTest = { data: insertTest, error: insertError }; + + // If insert succeeded, delete the test website + if (insertTest && insertTest.length > 0) { + const { data: deleteTest, error: deleteError } = await supabase + .from("websites") + .delete() + .eq("id", insertTest[0].id); + diagnosticResults.deleteTest = { data: deleteTest, error: deleteError }; + } + + setResults(diagnosticResults); + } catch (error) { + diagnosticResults.error = String(error); + setResults(diagnosticResults); + } finally { + setIsRunning(false); + } + }; + + return ( + <div className="container mx-auto p-6"> + <h1 className="text-2xl font-bold mb-6">Database Diagnostics</h1> + + <Card> + <CardContent className="p-6"> + <div className="mb-4"> + <Button onClick={runDiagnostics} disabled={isRunning}> + {isRunning ? "Running Tests..." : "Run Diagnostics"} + </Button> + </div> + + <div className="mt-6"> + <h2 className="text-lg font-semibold mb-2">Results:</h2> + <pre className="bg-gray-100 p-4 rounded-md overflow-auto max-h-[600px] text-xs"> + {JSON.stringify(results, null, 2)} + </pre> + </div> + </CardContent> + </Card> + </div> + ); +} diff --git a/website-monitoring-frontend/src/app/dashboard/error.tsx b/website-monitoring-frontend/src/app/dashboard/error.tsx new file mode 100644 index 0000000..2b8f38e --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/error.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { Button } from "@/components/ui/forms/Button"; +import { AlertCircle } from "lucide-react"; + +export default function DashboardError({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + return ( + <DashboardLayout> + <div className="flex flex-col items-center justify-center min-h-[60vh]"> + <div className="text-red-500 mb-4"> + <AlertCircle className="h-12 w-12" /> + </div> + <h2 className="text-2xl font-bold text-gray-900 mb-2"> + Something went wrong! + </h2> + <p className="text-gray-600 mb-4">{error.message}</p> + <Button onClick={() => reset()}>Try again</Button> + </div> + </DashboardLayout> + ); +} diff --git a/website-monitoring-frontend/src/app/dashboard/loading.tsx b/website-monitoring-frontend/src/app/dashboard/loading.tsx new file mode 100644 index 0000000..071e7be --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/loading.tsx @@ -0,0 +1,12 @@ +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { LoadingSpinner } from "@/components/ui/feedback/LoadingSpinner"; + +export default function DashboardLoading() { + return ( + <DashboardLayout> + <div className="flex items-center justify-center min-h-[60vh]"> + <LoadingSpinner /> + </div> + </DashboardLayout> + ); +} diff --git a/website-monitoring-frontend/src/app/dashboard/monitoring/page.tsx b/website-monitoring-frontend/src/app/dashboard/monitoring/page.tsx new file mode 100644 index 0000000..487a4ad --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/monitoring/page.tsx @@ -0,0 +1,431 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Activity, + CheckCircle, + XCircle, + Clock, + AlertTriangle, + Play, + Pause, + Settings, + TrendingUp, + TrendingDown, + Zap, + Loader2, +} from "lucide-react"; +import { useAuth } from "@/contexts/AuthContext"; +import { supabase } from "@/lib/supabase"; +import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils"; + +interface MonitoringStatus { + id: string; + website_name: string; + website_url: string; + is_monitoring: boolean; + last_check: string; + status: "up" | "down" | "warning"; + response_time: number; + uptime_percentage: number; + incidents_count: number; + created_at: string; +} + +interface UptimeMetric { + website_id: string; + timestamp: string; + status: "up" | "down" | "warning"; + response_time: number; + error_message?: string; +} + +export default function MonitoringPage() { + const { userDetails } = useAuth(); + const [websites, setWebsites] = useState<MonitoringStatus[]>([]); + const [recentChecks, setRecentChecks] = useState<UptimeMetric[]>([]); + const [loading, setLoading] = useState(true); + const [updating, setUpdating] = useState<string | null>(null); + + useEffect(() => { + if (userDetails?.organization_id) { + loadMonitoringData(); + // Set up real-time updates + const interval = setInterval(loadMonitoringData, 30000); // Update every 30 seconds + return () => clearInterval(interval); + } + }, [userDetails]); + + const loadMonitoringData = async () => { + if (!userDetails?.organization_id) return; + + try { + setLoading(true); + + // Fetch websites with monitoring status + const { data: websitesData, error: websitesError } = await supabase + .from("websites") + .select(` + id, + name, + base_url, + is_active, + created_at, + uptime_checks ( + id, + status, + response_time, + checked_at, + error_message + ) + `) + .eq("organization_id", userDetails.organization_id) + .eq("is_active", true) + .order("created_at", { ascending: false }); + + if (websitesError) throw websitesError; + + // Process monitoring data + const monitoringData: MonitoringStatus[] = websitesData?.map((website: any) => { + const checks = website.uptime_checks || []; + const recentChecks = checks + .filter((check: any) => { + const checkDate = new Date(check.checked_at); + const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + return checkDate >= dayAgo; + }) + .sort((a: any, b: any) => new Date(b.checked_at).getTime() - new Date(a.checked_at).getTime()); + + const latestCheck = recentChecks[0]; + const upChecks = recentChecks.filter((check: any) => check.status === "up").length; + const totalChecks = recentChecks.length; + const uptimePercentage = totalChecks > 0 ? Math.round((upChecks / totalChecks) * 100) : 0; + const incidents = recentChecks.filter((check: any) => check.status === "down").length; + + return { + id: website.id, + website_name: website.name, + website_url: website.base_url, + is_monitoring: true, // Assume monitoring is enabled for active websites + last_check: latestCheck?.checked_at || website.created_at, + status: latestCheck?.status || "warning", + response_time: latestCheck?.response_time || 0, + uptime_percentage: uptimePercentage, + incidents_count: incidents, + created_at: website.created_at, + }; + }) || []; + + setWebsites(monitoringData); + + // Get recent checks for the timeline + const allChecks: UptimeMetric[] = []; + websitesData?.forEach((website: any) => { + const checks = website.uptime_checks || []; + checks.slice(0, 10).forEach((check: any) => { + allChecks.push({ + website_id: website.id, + timestamp: check.checked_at, + status: check.status, + response_time: check.response_time, + error_message: check.error_message, + }); + }); + }); + + allChecks.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + setRecentChecks(allChecks.slice(0, 20)); + + } catch (error) { + const errorInfo = extractSupabaseErrorInfo(error); + logError("Error loading monitoring data", error, { + organizationId: userDetails.organization_id, + function: "loadMonitoringData", + supabaseError: errorInfo + }); + } finally { + setLoading(false); + } + }; + + const toggleMonitoring = async (websiteId: string, currentStatus: boolean) => { + try { + setUpdating(websiteId); + + // In a real implementation, you would update monitoring settings + // For now, we'll just simulate the action + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Update local state + setWebsites(prev => prev.map(website => + website.id === websiteId + ? { ...website, is_monitoring: !currentStatus } + : website + )); + + } catch (error) { + console.error("Error toggling monitoring:", error); + } finally { + setUpdating(null); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case "up": + return <CheckCircle className="w-4 h-4 text-green-500" />; + case "down": + return <XCircle className="w-4 h-4 text-red-500" />; + case "warning": + return <AlertTriangle className="w-4 h-4 text-yellow-500" />; + default: + return <Clock className="w-4 h-4 text-gray-500" />; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "up": + return "bg-green-100 text-green-800"; + case "down": + return "bg-red-100 text-red-800"; + case "warning": + return "bg-yellow-100 text-yellow-800"; + default: + return "bg-gray-100 text-gray-800"; + } + }; + + const getUptimeColor = (percentage: number) => { + if (percentage >= 99) return "text-green-600"; + if (percentage >= 95) return "text-yellow-600"; + return "text-red-600"; + }; + + if (loading) { + return ( + <DashboardLayout> + <div className="flex items-center justify-center py-12"> + <Loader2 className="w-8 h-8 animate-spin text-blue-600" /> + </div> + </DashboardLayout> + ); + } + + const totalWebsites = websites.length; + const activeMonitoring = websites.filter(w => w.is_monitoring).length; + const upWebsites = websites.filter(w => w.status === "up").length; + const downWebsites = websites.filter(w => w.status === "down").length; + const avgUptime = websites.length > 0 + ? Math.round(websites.reduce((sum, w) => sum + w.uptime_percentage, 0) / websites.length) + : 0; + + return ( + <DashboardLayout> + <div className="space-y-6"> + {/* Header */} + <div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between"> + <div> + <h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2"> + <Activity className="w-6 h-6" /> + Website Monitoring + </h1> + <p className="text-gray-600 mt-1"> + Real-time uptime monitoring for your websites + </p> + </div> + <Button + onClick={loadMonitoringData} + className="flex items-center gap-2" + > + <Zap className="w-4 h-4" /> + Refresh Status + </Button> + </div> + + {/* Summary Cards */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Total Websites</p> + <p className="text-2xl font-bold">{totalWebsites}</p> + </div> + <Activity className="w-8 h-8 text-blue-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Monitoring Active</p> + <p className="text-2xl font-bold text-green-600">{activeMonitoring}</p> + </div> + <CheckCircle className="w-8 h-8 text-green-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Websites Up</p> + <p className="text-2xl font-bold text-green-600">{upWebsites}</p> + </div> + <TrendingUp className="w-8 h-8 text-green-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Average Uptime</p> + <p className={`text-2xl font-bold ${getUptimeColor(avgUptime)}`}> + {avgUptime}% + </p> + </div> + <TrendingUp className="w-8 h-8 text-purple-600" /> + </div> + </CardContent> + </Card> + </div> + + {/* Monitoring Status */} + <div className="space-y-4"> + <h2 className="text-lg font-semibold text-gray-900">Website Status</h2> + + {websites.length > 0 ? ( + <div className="grid gap-4"> + {websites.map((website) => ( + <Card key={website.id}> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-4"> + <div className="flex items-center gap-2"> + {getStatusIcon(website.status)} + <div> + <h3 className="font-semibold text-gray-900">{website.website_name}</h3> + <p className="text-sm text-gray-500">{website.website_url}</p> + </div> + </div> + </div> + + <div className="flex items-center gap-4"> + <div className="text-right"> + <Badge className={getStatusColor(website.status)}> + {website.status.toUpperCase()} + </Badge> + <p className="text-xs text-gray-500 mt-1"> + Last check: {new Date(website.last_check).toLocaleTimeString()} + </p> + </div> + + <div className="text-right"> + <p className="text-sm font-medium"> + {website.response_time > 0 ? `${website.response_time}ms` : "—"} + </p> + <p className="text-xs text-gray-500">Response time</p> + </div> + + <div className="text-right"> + <p className={`text-sm font-medium ${getUptimeColor(website.uptime_percentage)}`}> + {website.uptime_percentage}% + </p> + <p className="text-xs text-gray-500">24h uptime</p> + </div> + + <div className="text-right"> + <p className="text-sm font-medium text-red-600"> + {website.incidents_count} + </p> + <p className="text-xs text-gray-500">Incidents</p> + </div> + + <Button + variant="outline" + size="sm" + onClick={() => toggleMonitoring(website.id, website.is_monitoring)} + disabled={updating === website.id} + className="flex items-center gap-1" + > + {updating === website.id ? ( + <Loader2 className="w-3 h-3 animate-spin" /> + ) : website.is_monitoring ? ( + <Pause className="w-3 h-3" /> + ) : ( + <Play className="w-3 h-3" /> + )} + {website.is_monitoring ? "Pause" : "Start"} + </Button> + </div> + </div> + </CardContent> + </Card> + ))} + </div> + ) : ( + <Card> + <CardContent className="p-12 text-center"> + <Activity className="w-16 h-16 mx-auto text-gray-400 mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + No websites being monitored + </h3> + <p className="text-gray-600"> + Add websites and enable monitoring to see uptime status here + </p> + </CardContent> + </Card> + )} + </div> + + {/* Recent Activity */} + {recentChecks.length > 0 && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Clock className="w-5 h-5" /> + Recent Activity + </CardTitle> + </CardHeader> + <CardContent> + <div className="space-y-3"> + {recentChecks.slice(0, 10).map((check, index) => { + const website = websites.find(w => w.id === check.website_id); + return ( + <div key={index} className="flex items-center justify-between py-2 border-b border-gray-100 last:border-0"> + <div className="flex items-center gap-3"> + {getStatusIcon(check.status)} + <div> + <p className="text-sm font-medium">{website?.website_name || "Unknown"}</p> + <p className="text-xs text-gray-500"> + {new Date(check.timestamp).toLocaleString()} + </p> + </div> + </div> + <div className="text-right"> + <p className="text-sm">{check.response_time}ms</p> + {check.error_message && ( + <p className="text-xs text-red-500">{check.error_message}</p> + )} + </div> + </div> + ); + })} + </div> + </CardContent> + </Card> + )} + </div> + </DashboardLayout> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/dashboard/organizations/[id]/settings/page.tsx b/website-monitoring-frontend/src/app/dashboard/organizations/[id]/settings/page.tsx new file mode 100644 index 0000000..6771e5c --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/organizations/[id]/settings/page.tsx @@ -0,0 +1,448 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { useAuth } from "@/contexts/AuthContext"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Input } from "@/components/ui/forms/Input"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, +} from "@/components/ui/forms/Form"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/layout/Tabs"; +import { TeamManagement } from "@/components/dashboard/TeamManagement"; +import { + Building2, + Settings, + Users, + CreditCard, + Shield, + Trash2, + ArrowLeft, + Save, + Loader2, + AlertCircle, + Check, +} from "lucide-react"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { motion, AnimatePresence } from "framer-motion"; +import { supabase } from "@/lib/supabase"; + +interface Organization { + id: string; + name: string; + subscription_tier: string; + subscription_status: string; + created_at: string; +} + +const organizationFormSchema = z.object({ + name: z.string().min(2, "Organization name must be at least 2 characters"), +}); + +export default function OrganizationSettingsPage() { + const params = useParams(); + const router = useRouter(); + const { user, userDetails } = useAuth(); + const organizationId = params.id as string; + + const [organization, setOrganization] = useState<Organization | null>(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [activeTab, setActiveTab] = useState("general"); + + const form = useForm<z.infer<typeof organizationFormSchema>>({ + resolver: zodResolver(organizationFormSchema), + defaultValues: { name: "" }, + }); + + useEffect(() => { + if (organizationId && user) { + loadOrganization(); + } + }, [organizationId, user]); + + const loadOrganization = async () => { + try { + setLoading(true); + + // Check if user has access to this organization + if (userDetails?.organization_id !== organizationId) { + setError("Access denied. You don't have permission to access this organization."); + return; + } + + const { data: org, error: orgError } = await supabase + .from("organizations") + .select("*") + .eq("id", organizationId) + .single(); + + if (orgError) { + throw orgError; + } + + setOrganization(org); + form.setValue("name", org.name); + } catch (error) { + console.error("Error loading organization:", error); + setError("Failed to load organization details"); + } finally { + setLoading(false); + } + }; + + const handleUpdateOrganization = async (values: z.infer<typeof organizationFormSchema>) => { + try { + setSaving(true); + setError(""); + setSuccess(""); + + const { error } = await supabase + .from("organizations") + .update({ name: values.name }) + .eq("id", organizationId); + + if (error) { + throw error; + } + + setOrganization(prev => prev ? { ...prev, name: values.name } : null); + setSuccess("Organization updated successfully!"); + } catch (error) { + console.error("Error updating organization:", error); + setError("Failed to update organization"); + } finally { + setSaving(false); + } + }; + + const handleDeleteOrganization = async () => { + if (!confirm("Are you sure you want to delete this organization? This action cannot be undone and will remove all associated data.")) { + return; + } + + const confirmText = prompt("Type 'DELETE' to confirm:"); + if (confirmText !== "DELETE") { + return; + } + + try { + const { error } = await supabase + .from("organizations") + .delete() + .eq("id", organizationId); + + if (error) { + throw error; + } + + router.push("/dashboard/organizations"); + } catch (error) { + console.error("Error deleting organization:", error); + setError("Failed to delete organization"); + } + }; + + const getTierColor = (tier: string) => { + switch (tier) { + case "pro": return "text-blue-600 bg-blue-100"; + case "enterprise": return "text-purple-600 bg-purple-100"; + default: return "text-gray-600 bg-gray-100"; + } + }; + + const canManageOrganization = userDetails?.role === "owner"; + + if (loading) { + return ( + <DashboardLayout> + <div className="flex items-center justify-center h-64"> + <Loader2 className="w-8 h-8 animate-spin text-blue-600" /> + </div> + </DashboardLayout> + ); + } + + if (!organization) { + return ( + <DashboardLayout> + <div className="text-center py-12"> + <AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + Organization Not Found + </h3> + <p className="text-gray-600 mb-6"> + The organization you're looking for doesn't exist or you don't have access to it. + </p> + <Button onClick={() => router.push("/dashboard/organizations")}> + <ArrowLeft className="w-4 h-4 mr-2" /> + Back to Organizations + </Button> + </div> + </DashboardLayout> + ); + } + + return ( + <DashboardLayout> + <div className="max-w-4xl mx-auto py-8"> + {/* Header */} + <div className="flex items-center gap-4 mb-8"> + <Button + variant="outline" + onClick={() => router.push("/dashboard/organizations")} + className="flex items-center gap-2" + > + <ArrowLeft className="w-4 h-4" /> + Back + </Button> + <div> + <h1 className="text-3xl font-bold text-gray-900">{organization.name}</h1> + <p className="text-gray-600 mt-1">Organization Settings</p> + </div> + </div> + + {/* Success/Error Messages */} + <AnimatePresence> + {success && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + className="bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-3 mb-6" + > + <Check className="w-5 h-5 text-green-500" /> + <span className="text-green-800">{success}</span> + <Button + variant="outline" + size="sm" + onClick={() => setSuccess("")} + className="ml-auto" + > + Dismiss + </Button> + </motion.div> + )} + + {error && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3 mb-6" + > + <AlertCircle className="w-5 h-5 text-red-500" /> + <span className="text-red-800">{error}</span> + <Button + variant="outline" + size="sm" + onClick={() => setError("")} + className="ml-auto" + > + Dismiss + </Button> + </motion.div> + )} + </AnimatePresence> + + {/* Settings Tabs */} + <Tabs value={activeTab} onValueChange={setActiveTab}> + <TabsList className="grid w-full grid-cols-4"> + <TabsTrigger value="general" className="flex items-center gap-2"> + <Settings className="w-4 h-4" /> + General + </TabsTrigger> + <TabsTrigger value="members" className="flex items-center gap-2"> + <Users className="w-4 h-4" /> + Members + </TabsTrigger> + <TabsTrigger value="billing" className="flex items-center gap-2"> + <CreditCard className="w-4 h-4" /> + Billing + </TabsTrigger> + <TabsTrigger value="danger" className="flex items-center gap-2"> + <Shield className="w-4 h-4" /> + Danger Zone + </TabsTrigger> + </TabsList> + + {/* General Settings */} + <TabsContent value="general" className="space-y-6"> + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Building2 className="w-5 h-5 text-blue-600" /> + Organization Details + </CardTitle> + </CardHeader> + <CardContent> + <Form {...form}> + <form + onSubmit={form.handleSubmit(handleUpdateOrganization)} + className="space-y-4" + > + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>Organization Name</FormLabel> + <FormControl> + <Input + {...field} + placeholder="Organization Name" + disabled={!canManageOrganization || saving} + /> + </FormControl> + <FormDescription> + This name will be visible to all team members + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* Organization Info */} + <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4"> + <div> + <label className="text-sm font-medium text-gray-700"> + Subscription Tier + </label> + <div className="mt-1"> + <span className={`inline-block px-3 py-1 rounded-full text-sm ${getTierColor(organization.subscription_tier)}`}> + {organization.subscription_tier.charAt(0).toUpperCase() + organization.subscription_tier.slice(1)} + </span> + </div> + </div> + <div> + <label className="text-sm font-medium text-gray-700"> + Created + </label> + <div className="mt-1 text-sm text-gray-900"> + {new Date(organization.created_at).toLocaleDateString()} + </div> + </div> + </div> + + {canManageOrganization && ( + <Button type="submit" disabled={saving}> + {saving ? ( + <> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + Saving... + </> + ) : ( + <> + <Save className="w-4 h-4 mr-2" /> + Save Changes + </> + )} + </Button> + )} + </form> + </Form> + </CardContent> + </Card> + </TabsContent> + + {/* Team Members */} + <TabsContent value="members"> + <TeamManagement organizationId={organizationId} /> + </TabsContent> + + {/* Billing */} + <TabsContent value="billing"> + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <CreditCard className="w-5 h-5 text-blue-600" /> + Billing & Subscription + </CardTitle> + </CardHeader> + <CardContent> + <div className="text-center py-8"> + <CreditCard className="w-12 h-12 text-gray-400 mx-auto mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + Billing Management + </h3> + <p className="text-gray-600 mb-6"> + Billing and subscription management features will be available soon. + </p> + <div className="p-4 bg-blue-50 border border-blue-200 rounded-lg"> + <div className="flex items-center justify-between"> + <div> + <div className="font-medium text-blue-900">Current Plan</div> + <div className="text-sm text-blue-700"> + {organization.subscription_tier.charAt(0).toUpperCase() + organization.subscription_tier.slice(1)} Plan + </div> + </div> + <Button variant="outline" disabled> + Manage Billing + </Button> + </div> + </div> + </div> + </CardContent> + </Card> + </TabsContent> + + {/* Danger Zone */} + <TabsContent value="danger"> + <Card className="border-red-200"> + <CardHeader> + <CardTitle className="flex items-center gap-2 text-red-600"> + <Shield className="w-5 h-5" /> + Danger Zone + </CardTitle> + </CardHeader> + <CardContent> + <div className="space-y-4"> + <div className="p-4 bg-red-50 border border-red-200 rounded-lg"> + <h3 className="font-semibold text-red-900 mb-2"> + Delete Organization + </h3> + <p className="text-red-700 text-sm mb-4"> + Once you delete an organization, there is no going back. Please be certain. + This will permanently delete all associated websites, data, and team member access. + </p> + {canManageOrganization ? ( + <Button + variant="destructive" + onClick={handleDeleteOrganization} + className="flex items-center gap-2" + > + <Trash2 className="w-4 h-4" /> + Delete Organization + </Button> + ) : ( + <p className="text-sm text-red-600"> + Only organization owners can delete the organization. + </p> + )} + </div> + </div> + </CardContent> + </Card> + </TabsContent> + </Tabs> + </div> + </DashboardLayout> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/dashboard/organizations/new/page.tsx b/website-monitoring-frontend/src/app/dashboard/organizations/new/page.tsx new file mode 100644 index 0000000..abdc636 --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/organizations/new/page.tsx @@ -0,0 +1,431 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/contexts/AuthContext"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Input } from "@/components/ui/forms/Input"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, +} from "@/components/ui/forms/Form"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Shield, + Check, + Building2, + Users, + Star, + ArrowRight, + Loader2, + Zap, + Globe, + BarChart3, + AlertCircle, +} from "lucide-react"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { motion, AnimatePresence } from "framer-motion"; + +const formSchema = z.object({ + name: z.string().min(2, "Organization name must be at least 2 characters"), +}); + +const features = [ + { + icon: Globe, + title: "Website Monitoring", + description: + "Monitor unlimited websites with real-time performance tracking", + }, + { + icon: BarChart3, + title: "Advanced Analytics", + description: + "Get detailed insights into performance, SEO, and accessibility", + }, + { + icon: Users, + title: "Team Collaboration", + description: "Invite team members and manage access permissions", + }, + { + icon: Zap, + title: "Automated Alerts", + description: "Receive instant notifications when issues are detected", + }, +]; + +const plans = [ + { + name: "Free", + price: "$0", + period: "forever", + description: "Perfect for getting started", + features: [ + "Up to 3 websites", + "Basic monitoring", + "Email alerts", + "7-day data retention", + ], + current: true, + }, + { + name: "Pro", + price: "$29", + period: "per month", + description: "For growing businesses", + features: [ + "Up to 25 websites", + "Advanced monitoring", + "Real-time alerts", + "90-day data retention", + "Team collaboration", + ], + popular: true, + }, + { + name: "Enterprise", + price: "Custom", + period: "pricing", + description: "For large organizations", + features: [ + "Unlimited websites", + "Custom integrations", + "Priority support", + "Unlimited data retention", + "SSO & compliance", + ], + }, +]; + +export default function NewOrganizationPage() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + const [step, setStep] = useState(1); + const router = useRouter(); + const { user, createOrganizationForUser } = useAuth(); + + const form = useForm<z.infer<typeof formSchema>>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: user?.user_metadata?.name + ? `${user.user_metadata.name}'s Organization` + : "My Organization", + }, + }); + + const onSubmit = async (values: z.infer<typeof formSchema>) => { + if (!user) { + setError("You must be logged in to create an organization"); + return; + } + + try { + setLoading(true); + setError(""); + + const orgId = await createOrganizationForUser(user.id, values.name); + + if (!orgId) { + throw new Error("Failed to create organization"); + } + + setSuccess(true); + setStep(3); + + // Redirect after a short delay + setTimeout(() => { + router.push("/dashboard"); + }, 2000); + } catch (err: unknown) { + console.error("Failed to create organization:", err); + setError( + err instanceof Error ? err.message : "Failed to create organization", + ); + } finally { + setLoading(false); + } + }; + + const handleContinue = () => { + setStep(2); + }; + + return ( + <DashboardLayout> + <div className="max-w-6xl mx-auto py-8"> + <AnimatePresence mode="wait"> + {/* Step 1: Welcome */} + {step === 1 && ( + <motion.div + key="step1" + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -20 }} + className="text-center space-y-8" + > + <div> + <motion.div + initial={{ scale: 0 }} + animate={{ scale: 1 }} + transition={{ delay: 0.2, type: "spring", stiffness: 200 }} + className="w-20 h-20 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-6" + > + <Building2 className="w-10 h-10 text-blue-600" /> + </motion.div> + <h1 className="text-4xl font-bold text-gray-900 mb-4"> + Welcome to CloudLense + </h1> + <p className="text-xl text-gray-600 max-w-2xl mx-auto"> + Set up your organization to start monitoring your websites and + gain valuable insights into performance, SEO, and + accessibility. + </p> + </div> + + {/* Features Grid */} + <div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto"> + {features.map((feature, index) => { + const Icon = feature.icon; + return ( + <motion.div + key={feature.title} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.3 + index * 0.1 }} + > + <Card className="text-left hover:shadow-lg transition-shadow"> + <CardContent className="p-6"> + <div className="flex items-start gap-4"> + <div className="p-2 bg-blue-100 rounded-lg"> + <Icon className="w-6 h-6 text-blue-600" /> + </div> + <div> + <h3 className="font-semibold text-gray-900 mb-2"> + {feature.title} + </h3> + <p className="text-sm text-gray-600"> + {feature.description} + </p> + </div> + </div> + </CardContent> + </Card> + </motion.div> + ); + })} + </div> + + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.7 }} + > + <Button + size="lg" + onClick={handleContinue} + className="px-8 flex items-center gap-2" + > + Get Started + <ArrowRight className="w-4 h-4" /> + </Button> + </motion.div> + </motion.div> + )} + + {/* Step 2: Organization Setup */} + {step === 2 && ( + <motion.div + key="step2" + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -20 }} + className="max-w-2xl mx-auto" + > + <div className="text-center mb-8"> + <h1 className="text-3xl font-bold text-gray-900 mb-4"> + Create Your Organization + </h1> + <p className="text-gray-600"> + Set up your organization to start monitoring websites + </p> + </div> + + <Card className="border-0 shadow-lg"> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Building2 className="w-5 h-5 text-blue-600" /> + Organization Details + </CardTitle> + </CardHeader> + <CardContent> + <Form {...form}> + <form + onSubmit={form.handleSubmit(onSubmit)} + className="space-y-6" + > + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>Organization Name</FormLabel> + <FormControl> + <Input {...field} placeholder="My Organization" /> + </FormControl> + <FormDescription> + This name will be visible to all team members + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* Plan Selection */} + <div className="space-y-4"> + <h3 className="text-lg font-semibold"> + Choose Your Plan + </h3> + <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> + {plans.map((plan) => ( + <div + key={plan.name} + className={`relative border rounded-lg p-4 ${ + plan.current + ? "border-blue-500 bg-blue-50" + : "border-gray-200" + }`} + > + {plan.popular && ( + <Badge className="absolute -top-2 left-1/2 -translate-x-1/2"> + <Star className="w-3 h-3 mr-1" /> + Popular + </Badge> + )} + <div className="text-center"> + <h4 className="font-semibold">{plan.name}</h4> + <div className="mt-2"> + <span className="text-2xl font-bold"> + {plan.price} + </span> + <span className="text-sm text-gray-500"> + /{plan.period} + </span> + </div> + <p className="text-sm text-gray-600 mt-2"> + {plan.description} + </p> + </div> + <ul className="mt-4 space-y-2"> + {plan.features.map((feature) => ( + <li + key={feature} + className="flex items-center text-sm" + > + <Check className="w-3 h-3 text-green-500 mr-2" /> + {feature} + </li> + ))} + </ul> + {plan.current && ( + <Badge + variant="blue" + className="w-full justify-center mt-3" + > + Current Plan + </Badge> + )} + </div> + ))} + </div> + <p className="text-xs text-gray-500 text-center"> + You can upgrade or downgrade your plan at any time + </p> + </div> + + {error && ( + <div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3"> + <AlertCircle className="w-5 h-5 text-red-500" /> + <span className="text-red-800">{error}</span> + </div> + )} + + <div className="flex justify-between"> + <Button + type="button" + variant="outline" + onClick={() => setStep(1)} + disabled={loading} + > + Back + </Button> + <Button type="submit" disabled={loading}> + {loading ? ( + <> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + Creating... + </> + ) : ( + "Create Organization" + )} + </Button> + </div> + </form> + </Form> + </CardContent> + </Card> + </motion.div> + )} + + {/* Step 3: Success */} + {step === 3 && ( + <motion.div + key="step3" + initial={{ opacity: 0, scale: 0.95 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0.95 }} + className="text-center py-12" + > + <motion.div + initial={{ scale: 0 }} + animate={{ scale: 1 }} + transition={{ delay: 0.2, type: "spring", stiffness: 200 }} + className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6" + > + <Check className="w-10 h-10 text-green-600" /> + </motion.div> + <h1 className="text-3xl font-bold text-gray-900 mb-4"> + Organization Created Successfully! + </h1> + <p className="text-gray-600 mb-8 max-w-md mx-auto"> + Your organization has been set up. You can now start adding + websites and inviting team members to collaborate. + </p> + <div className="flex items-center justify-center gap-2"> + <Loader2 className="w-4 h-4 animate-spin text-blue-600" /> + <span className="text-sm text-blue-600"> + Redirecting to dashboard... + </span> + </div> + </motion.div> + )} + </AnimatePresence> + </div> + </DashboardLayout> + ); +} diff --git a/website-monitoring-frontend/src/app/dashboard/organizations/page.tsx b/website-monitoring-frontend/src/app/dashboard/organizations/page.tsx new file mode 100644 index 0000000..5997b16 --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/organizations/page.tsx @@ -0,0 +1,492 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/contexts/AuthContext"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Input } from "@/components/ui/forms/Input"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/components/ui/forms/Form"; +import { + Building2, + Users, + Settings, + Trash2, + Edit, + Plus, + Calendar, + Crown, + Loader2, + AlertCircle, +} from "lucide-react"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { motion, AnimatePresence } from "framer-motion"; +import { supabase } from "@/lib/supabase"; + +interface Organization { + id: string; + name: string; + subscription_tier: string; + subscription_status: string; + created_at: string; + member_count: number; + website_count: number; + user_role: string; +} + +const editFormSchema = z.object({ + name: z.string().min(2, "Organization name must be at least 2 characters"), +}); + +export default function OrganizationsPage() { + const [organizations, setOrganizations] = useState<Organization[]>([]); + const [loading, setLoading] = useState(true); + const [editingOrg, setEditingOrg] = useState<Organization | null>(null); + const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null); + const [error, setError] = useState(""); + const router = useRouter(); + const { user, userDetails } = useAuth(); + + const editForm = useForm<z.infer<typeof editFormSchema>>({ + resolver: zodResolver(editFormSchema), + defaultValues: { name: "" }, + }); + + useEffect(() => { + if (user) { + loadOrganizations(); + } + }, [user]); + + const loadOrganizations = async () => { + try { + setLoading(true); + + // Get organizations where user is a member + const { data: userOrgs, error: userOrgError } = await supabase + .from("users") + .select(` + organization_id, + role, + organizations ( + id, + name, + subscription_tier, + subscription_status, + created_at + ) + `) + .eq("id", user?.id); + + if (userOrgError) throw userOrgError; + + // Get organization stats + const orgIds = userOrgs?.map(u => u.organization_id).filter(Boolean) || []; + + const [membersData, websitesData] = await Promise.all([ + // Get member counts + supabase + .from("users") + .select("organization_id") + .in("organization_id", orgIds), + + // Get website counts + supabase + .from("websites") + .select("organization_id") + .in("organization_id", orgIds) + ]); + + const memberCounts = membersData.data?.reduce((acc, member) => { + acc[member.organization_id] = (acc[member.organization_id] || 0) + 1; + return acc; + }, {} as Record<string, number>) || {}; + + const websiteCounts = websitesData.data?.reduce((acc, website) => { + acc[website.organization_id] = (acc[website.organization_id] || 0) + 1; + return acc; + }, {} as Record<string, number>) || {}; + + const orgsWithStats = (userOrgs as Array<any> | undefined)?.map((userOrg: any) => ({ + id: userOrg.organizations?.id || "", + name: userOrg.organizations?.name || "", + subscription_tier: userOrg.organizations?.subscription_tier || "free", + subscription_status: userOrg.organizations?.subscription_status || "active", + created_at: userOrg.organizations?.created_at || "", + member_count: memberCounts[String(userOrg.organization_id)] || 0, + website_count: websiteCounts[String(userOrg.organization_id)] || 0, + user_role: userOrg.role || "member", + })).filter((org: any) => org.id) || []; + + setOrganizations(orgsWithStats); + } catch (error) { + console.error("Error loading organizations:", error); + setError("Failed to load organizations"); + } finally { + setLoading(false); + } + }; + + const handleEditOrganization = async (values: z.infer<typeof editFormSchema>) => { + if (!editingOrg) return; + + try { + const { error } = await supabase + .from("organizations") + .update({ name: values.name }) + .eq("id", editingOrg.id); + + if (error) throw error; + + // Update local state + setOrganizations(orgs => + orgs.map(org => + org.id === editingOrg.id + ? { ...org, name: values.name } + : org + ) + ); + + setEditingOrg(null); + editForm.reset(); + } catch (error) { + console.error("Error updating organization:", error); + setError("Failed to update organization"); + } + }; + + const handleDeleteOrganization = async (orgId: string) => { + try { + // First, check if user is owner + const org = organizations.find(o => o.id === orgId); + if (org?.user_role !== "owner") { + setError("Only organization owners can delete organizations"); + return; + } + + // Delete organization (cascade should handle related records) + const { error } = await supabase + .from("organizations") + .delete() + .eq("id", orgId); + + if (error) throw error; + + // Update local state + setOrganizations(orgs => orgs.filter(org => org.id !== orgId)); + setDeleteConfirm(null); + + // If this was the user's current organization, they might need to select a new one + if (userDetails?.organization_id === orgId) { + router.push("/dashboard/organizations/new"); + } + } catch (error) { + console.error("Error deleting organization:", error); + setError("Failed to delete organization"); + } + }; + + const startEdit = (org: Organization) => { + setEditingOrg(org); + editForm.setValue("name", org.name); + }; + + const getTierColor = (tier: string) => { + switch (tier) { + case "pro": return "text-blue-600 bg-blue-100"; + case "enterprise": return "text-purple-600 bg-purple-100"; + default: return "text-gray-600 bg-gray-100"; + } + }; + + if (loading) { + return ( + <DashboardLayout> + <div className="flex items-center justify-center h-64"> + <Loader2 className="w-8 h-8 animate-spin text-blue-600" /> + </div> + </DashboardLayout> + ); + } + + return ( + <DashboardLayout> + <div className="max-w-6xl mx-auto py-8"> + <div className="flex items-center justify-between mb-8"> + <div> + <h1 className="text-3xl font-bold text-gray-900">Organizations</h1> + <p className="text-gray-600 mt-2"> + Manage your organizations and team settings + </p> + </div> + <Button + onClick={() => router.push("/dashboard/organizations/new")} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + New Organization + </Button> + </div> + + {error && ( + <div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3 mb-6"> + <AlertCircle className="w-5 h-5 text-red-500" /> + <span className="text-red-800">{error}</span> + <Button + variant="outline" + size="sm" + onClick={() => setError("")} + className="ml-auto" + > + Dismiss + </Button> + </div> + )} + + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> + <AnimatePresence> + {organizations.map((org) => ( + <motion.div + key={org.id} + initial={{ opacity: 0, scale: 0.95 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0.95 }} + transition={{ duration: 0.2 }} + > + <Card className="hover:shadow-lg transition-shadow"> + <CardHeader> + <div className="flex items-start justify-between"> + <div className="flex items-center gap-3"> + <div className="p-2 bg-blue-100 rounded-lg"> + <Building2 className="w-5 h-5 text-blue-600" /> + </div> + <div> + <CardTitle className="text-lg">{org.name}</CardTitle> + <div className="flex items-center gap-2 mt-1"> + <span className={`text-xs px-2 py-1 rounded-full ${getTierColor(org.subscription_tier)}`}> + {org.subscription_tier.charAt(0).toUpperCase() + org.subscription_tier.slice(1)} + </span> + {org.user_role === "owner" && ( + <Crown className="w-3 h-3 text-yellow-500" /> + )} + </div> + </div> + </div> + </div> + </CardHeader> + + <CardContent> + <div className="space-y-3"> + {/* Stats */} + <div className="grid grid-cols-2 gap-4"> + <div className="text-center"> + <div className="text-2xl font-bold text-gray-900"> + {org.member_count} + </div> + <div className="text-xs text-gray-500 flex items-center justify-center gap-1"> + <Users className="w-3 h-3" /> + Members + </div> + </div> + <div className="text-center"> + <div className="text-2xl font-bold text-gray-900"> + {org.website_count} + </div> + <div className="text-xs text-gray-500 flex items-center justify-center gap-1"> + <Building2 className="w-3 h-3" /> + Websites + </div> + </div> + </div> + + {/* Created Date */} + <div className="text-xs text-gray-500 flex items-center gap-1"> + <Calendar className="w-3 h-3" /> + Created {new Date(org.created_at).toLocaleDateString()} + </div> + + {/* Actions */} + <div className="flex gap-2 pt-2"> + <Button + variant="outline" + size="sm" + onClick={() => startEdit(org)} + className="flex-1" + disabled={org.user_role !== "owner"} + > + <Edit className="w-3 h-3 mr-1" /> + Edit + </Button> + <Button + variant="outline" + size="sm" + onClick={() => router.push(`/dashboard/organizations/${org.id}/settings`)} + className="flex-1" + > + <Settings className="w-3 h-3 mr-1" /> + Settings + </Button> + {org.user_role === "owner" && ( + <Button + variant="outline" + size="sm" + onClick={() => setDeleteConfirm(org.id)} + className="text-red-600 hover:text-red-700 hover:bg-red-50" + > + <Trash2 className="w-3 h-3" /> + </Button> + )} + </div> + </div> + </CardContent> + </Card> + </motion.div> + ))} + </AnimatePresence> + </div> + + {organizations.length === 0 && ( + <div className="text-center py-12"> + <Building2 className="w-12 h-12 text-gray-400 mx-auto mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + No Organizations Found + </h3> + <p className="text-gray-600 mb-6"> + Create your first organization to start monitoring websites + </p> + <Button + onClick={() => router.push("/dashboard/organizations/new")} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + Create Organization + </Button> + </div> + )} + + {/* Edit Organization Modal */} + <AnimatePresence> + {editingOrg && ( + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + exit={{ opacity: 0 }} + className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" + > + <motion.div + initial={{ scale: 0.95, opacity: 0 }} + animate={{ scale: 1, opacity: 1 }} + exit={{ scale: 0.95, opacity: 0 }} + className="bg-white rounded-lg max-w-md w-full" + > + <Card className="border-0"> + <CardHeader> + <CardTitle>Edit Organization</CardTitle> + </CardHeader> + <CardContent> + <Form {...editForm}> + <form + onSubmit={editForm.handleSubmit(handleEditOrganization)} + className="space-y-4" + > + <FormField + control={editForm.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>Organization Name</FormLabel> + <FormControl> + <Input {...field} placeholder="Organization Name" /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <div className="flex gap-2 justify-end"> + <Button + type="button" + variant="outline" + onClick={() => { + setEditingOrg(null); + editForm.reset(); + }} + > + Cancel + </Button> + <Button type="submit"> + Update + </Button> + </div> + </form> + </Form> + </CardContent> + </Card> + </motion.div> + </motion.div> + )} + </AnimatePresence> + + {/* Delete Confirmation Modal */} + <AnimatePresence> + {deleteConfirm && ( + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + exit={{ opacity: 0 }} + className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" + > + <motion.div + initial={{ scale: 0.95, opacity: 0 }} + animate={{ scale: 1, opacity: 1 }} + exit={{ scale: 0.95, opacity: 0 }} + className="bg-white rounded-lg max-w-md w-full" + > + <Card className="border-0"> + <CardHeader> + <CardTitle className="text-red-600">Delete Organization</CardTitle> + </CardHeader> + <CardContent> + <p className="text-gray-600 mb-6"> + Are you sure you want to delete this organization? This action cannot be undone and will remove all associated websites and data. + </p> + + <div className="flex gap-2 justify-end"> + <Button + variant="outline" + onClick={() => setDeleteConfirm(null)} + > + Cancel + </Button> + <Button + variant="destructive" + onClick={() => handleDeleteOrganization(deleteConfirm)} + > + Delete Organization + </Button> + </div> + </CardContent> + </Card> + </motion.div> + </motion.div> + )} + </AnimatePresence> + </div> + </DashboardLayout> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/dashboard/page.tsx b/website-monitoring-frontend/src/app/dashboard/page.tsx new file mode 100644 index 0000000..6509aaa --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/page.tsx @@ -0,0 +1,571 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { useDashboardData } from "@/hooks/useDashboardData"; +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + BarChart3, + Globe, + Zap, + Search, + Plus, + TrendingUp, + TrendingDown, + Clock, + Shield, + Activity, + AlertCircle, + CheckCircle, + RefreshCw, + ExternalLink, + ArrowRight, +} from "lucide-react"; +import { useRouter } from "next/navigation"; +import { motion } from "framer-motion"; +import { supabase } from "@/lib/supabase"; +import { scanService } from "@/services/scanService"; +import { DatabaseSetupHelper } from "@/components/ui/DatabaseSetupHelper"; +import { SupabaseDiagnostic } from "@/components/ui/SupabaseDiagnostic"; + +interface DashboardStats { + websitesCount: number; + activePages: number; + totalScans: number; + averagePerformance: number; + lastScanTime: string; + recentScans: any[]; + websites: any[]; +} + +export default function DashboardPage() { + const { userDetails, organizationId, shouldShowLoading } = useDashboardData({ requireOrganization: false }); + const router = useRouter(); + const [stats, setStats] = useState<DashboardStats | null>(null); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + useEffect(() => { + if (organizationId) { + loadDashboardData(); + } else if (userDetails) { + // User exists but no organization, show empty dashboard + loadDashboardData(); + } + }, [organizationId, userDetails]); + + const loadDashboardData = async () => { + if (!userDetails?.organization_id) { + console.log("No organization_id yet, showing empty dashboard"); + setStats({ + websitesCount: 0, + activePages: 0, + totalScans: 0, + averagePerformance: 0, + lastScanTime: "Never", + recentScans: [], + websites: [], + }); + setLoading(false); + return; + } + + try { + setLoading(true); + + // Fetch websites + const { data: websites, error: websitesError } = await supabase + .from("websites") + .select( + ` + id, + name, + base_url, + is_active, + created_at, + pages!inner ( + id, + is_active + ) + `, + ) + .eq("organization_id", userDetails.organization_id) + .eq("is_active", true); + + if (websitesError) throw websitesError; + + // Fetch recent scans + let recentScans: any[] = []; + try { + recentScans = await scanService.getRecentScans(10); + } catch (error) { + console.log("No scans found yet:", error); + recentScans = []; + } + + // Calculate stats + const websitesCount = websites?.length || 0; + const activePages = + websites?.reduce( + (sum, website) => + sum + (website.pages?.filter((p: any) => p.is_active).length || 0), + 0, + ) || 0; + + const totalScans = recentScans.length; + const completedScans = recentScans.filter( + (scan) => scan.status === "completed", + ); + const averagePerformance = + completedScans.length > 0 + ? Math.round( + completedScans.reduce( + (sum, scan) => sum + (scan.performance_score || 0), + 0, + ) / completedScans.length, + ) + : 0; + + const lastScan = recentScans[0]; + const lastScanTime = lastScan + ? new Date(lastScan.created_at).toLocaleString() + : "Never"; + + setStats({ + websitesCount, + activePages, + totalScans, + averagePerformance, + lastScanTime, + recentScans: recentScans.slice(0, 5), + websites: websites || [], + }); + } catch (error) { + console.error("Failed to load dashboard data:", error); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + const handleRefresh = async () => { + setRefreshing(true); + await loadDashboardData(); + }; + + const getScoreColor = (score: number) => { + if (score >= 90) return "text-green-600"; + if (score >= 70) return "text-yellow-600"; + return "text-red-600"; + }; + + const getScoreBadgeColor = (score: number) => { + if (score >= 90) return "bg-green-100 text-green-800"; + if (score >= 70) return "bg-yellow-100 text-yellow-800"; + return "bg-red-100 text-red-800"; + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case "completed": + return <CheckCircle className="w-4 h-4 text-green-600" />; + case "running": + return <Activity className="w-4 h-4 text-blue-600 animate-pulse" />; + case "failed": + return <AlertCircle className="w-4 h-4 text-red-600" />; + default: + return <Clock className="w-4 h-4 text-gray-600" />; + } + }; + + // Show loading only when absolutely necessary + if (shouldShowLoading) { + return ( + <DashboardLayout> + <div className="flex items-center justify-center h-64"> + <div className="flex items-center space-x-2"> + <RefreshCw className="w-6 h-6 animate-spin text-blue-600" /> + <span className="text-gray-600">Loading dashboard...</span> + </div> + </div> + </DashboardLayout> + ); + } + + const quickStats = [ + { + label: "Websites Monitored", + value: stats?.websitesCount?.toString() || "0", + change: `${stats?.activePages || 0} active pages`, + trend: stats?.websitesCount ? "up" : "stable", + icon: Globe, + color: "blue", + }, + { + label: "Average Performance", + value: stats?.averagePerformance ? `${stats.averagePerformance}%` : "N/A", + change: + (stats?.averagePerformance ?? 0) >= 90 + ? "Excellent" + : (stats?.averagePerformance ?? 0) >= 70 + ? "Good" + : "Needs improvement", + trend: + (stats?.averagePerformance ?? 0) >= 90 + ? "up" + : (stats?.averagePerformance ?? 0) >= 70 + ? "stable" + : "down", + icon: Zap, + color: "green", + }, + { + label: "Total Scans", + value: stats?.totalScans?.toString() || "0", + change: "All time", + trend: "stable", + icon: Search, + color: "purple", + }, + { + label: "Last Scan", + value: stats?.lastScanTime === "Never" ? "Never" : "Recent", + change: stats?.lastScanTime || "No scans yet", + trend: "stable", + icon: Clock, + color: "gray", + }, + ]; + + return ( + <DashboardLayout> + <div className="space-y-8"> + {/* Database Setup Helper */} + <DatabaseSetupHelper /> + + {/* Supabase Diagnostic */} + <SupabaseDiagnostic /> + + {/* Welcome Header */} + <div className="flex items-center justify-between"> + <div> + <motion.h1 + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + className="text-3xl font-bold text-gray-900" + > + Welcome back, {userDetails?.name?.split(" ")[0] || "User"}! + </motion.h1> + <motion.p + initial={{ opacity: 0, y: -5 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.1 }} + className="text-gray-600 mt-2" + > + Monitor your website performance and SEO in real-time + </motion.p> + </div> + <motion.div + initial={{ opacity: 0, scale: 0.9 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ delay: 0.2 }} + className="flex gap-3" + > + <Button + variant="outline" + onClick={handleRefresh} + disabled={refreshing} + className="flex items-center gap-2" + > + <RefreshCw + className={`w-4 h-4 ${refreshing ? "animate-spin" : ""}`} + /> + Refresh + </Button> + <Button + onClick={() => router.push("/dashboard/websites/new")} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + Add Website + </Button> + </motion.div> + </div> + + {/* Quick Stats */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> + {quickStats.map((stat, index) => { + const Icon = stat.icon; + return ( + <motion.div + key={stat.label} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: index * 0.1 }} + > + <Card className="hover:shadow-lg transition-all duration-200 hover:scale-105"> + <CardContent className="p-6"> + <div className="flex items-start justify-between"> + <div className="flex-1"> + <p className="text-sm font-medium text-gray-600"> + {stat.label} + </p> + <p className="text-2xl font-bold text-gray-900 mt-1"> + {stat.value} + </p> + <div className="flex items-center mt-2"> + {stat.trend === "up" && ( + <TrendingUp className="w-3 h-3 text-green-500 mr-1" /> + )} + {stat.trend === "down" && ( + <TrendingDown className="w-3 h-3 text-red-500 mr-1" /> + )} + <p + className={`text-xs ${ + stat.trend === "up" + ? "text-green-600" + : stat.trend === "down" + ? "text-red-600" + : "text-gray-500" + }`} + > + {stat.change} + </p> + </div> + </div> + <div + className={`p-3 rounded-lg bg-${stat.color}-100 flex-shrink-0`} + > + <Icon className={`w-5 h-5 text-${stat.color}-600`} /> + </div> + </div> + </CardContent> + </Card> + </motion.div> + ); + })} + </div> + + {/* Main Content Grid */} + <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> + {/* Recent Scans */} + <motion.div + initial={{ opacity: 0, x: -20 }} + animate={{ opacity: 1, x: 0 }} + transition={{ delay: 0.4 }} + > + <Card className="h-full"> + <CardContent className="p-6"> + <div className="flex items-center justify-between mb-6"> + <div className="flex items-center gap-2"> + <BarChart3 className="w-5 h-5 text-blue-600" /> + <h2 className="text-xl font-semibold text-gray-900"> + Recent Scans + </h2> + </div> + <Button + variant="outline" + size="sm" + onClick={() => router.push("/dashboard/scans")} + className="flex items-center gap-1" + > + View All + <ArrowRight className="w-3 h-3" /> + </Button> + </div> + + <div className="space-y-4"> + {(stats?.recentScans?.length ?? 0) > 0 ? ( + stats?.recentScans?.map((scan, index) => ( + <motion.div + key={scan.id} + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.5 + index * 0.1 }} + className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors" + > + <div className="flex items-center space-x-3"> + {getStatusIcon(scan.status)} + <div> + <p className="font-medium text-gray-900 truncate max-w-48"> + {scan.pages?.title || + scan.pages?.url || + "Unknown Page"} + </p> + <p className="text-sm text-gray-500"> + {new Date(scan.created_at).toLocaleDateString()} + </p> + </div> + </div> + <div className="flex items-center space-x-2"> + {scan.performance_score && ( + <Badge + className={getScoreBadgeColor( + scan.performance_score, + )} + > + {scan.performance_score}% + </Badge> + )} + <Badge variant="gray" className="text-xs"> + {scan.status} + </Badge> + </div> + </motion.div> + )) + ) : ( + <div className="text-center py-8 text-gray-500"> + <Activity className="w-12 h-12 mx-auto mb-4 text-gray-400" /> + <p>No scans yet</p> + <p className="text-sm"> + Start monitoring your websites to see scan results + </p> + </div> + )} + </div> + </CardContent> + </Card> + </motion.div> + + {/* Websites Overview */} + <motion.div + initial={{ opacity: 0, x: 20 }} + animate={{ opacity: 1, x: 0 }} + transition={{ delay: 0.5 }} + > + <Card className="h-full"> + <CardContent className="p-6"> + <div className="flex items-center justify-between mb-6"> + <div className="flex items-center gap-2"> + <Globe className="w-5 h-5 text-green-600" /> + <h2 className="text-xl font-semibold text-gray-900"> + Your Websites + </h2> + </div> + <Button + variant="outline" + size="sm" + onClick={() => router.push("/dashboard/websites")} + className="flex items-center gap-1" + > + Manage All + <ArrowRight className="w-3 h-3" /> + </Button> + </div> + + <div className="space-y-4"> + {(stats?.websites?.length ?? 0) > 0 ? ( + stats?.websites?.slice(0, 5).map((website, index) => ( + <motion.div + key={website.id} + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.6 + index * 0.1 }} + className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer" + onClick={() => + router.push(`/dashboard/websites/${website.id}`) + } + > + <div className="flex items-center space-x-3"> + <div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center"> + <Globe className="w-4 h-4 text-blue-600" /> + </div> + <div> + <p className="font-medium text-gray-900"> + {website.name} + </p> + <p className="text-sm text-gray-500 flex items-center gap-1"> + {website.base_url} + <ExternalLink className="w-3 h-3" /> + </p> + </div> + </div> + <div className="flex items-center space-x-2"> + <Badge variant="green" className="text-xs"> + {website.pages?.filter((p: any) => p.is_active) + .length || 0}{" "} + pages + </Badge> + <Badge + className={ + website.is_active + ? "bg-green-100 text-green-800" + : "bg-gray-100 text-gray-800" + } + > + {website.is_active ? "Active" : "Inactive"} + </Badge> + </div> + </motion.div> + )) + ) : ( + <div className="text-center py-8 text-gray-500"> + <Globe className="w-12 h-12 mx-auto mb-4 text-gray-400" /> + <p>No websites added yet</p> + <Button + onClick={() => router.push("/dashboard/websites/new")} + className="mt-4 flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + Add Your First Website + </Button> + </div> + )} + </div> + </CardContent> + </Card> + </motion.div> + </div> + + {/* Quick Actions */} + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.7 }} + > + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + Quick Actions + </h3> + <p className="text-gray-600"> + Get started with monitoring your websites + </p> + </div> + <div className="flex gap-3"> + <Button + variant="outline" + onClick={() => router.push("/dashboard/websites")} + className="flex items-center gap-2" + > + <Globe className="w-4 h-4" /> + View Websites + </Button> + <Button + variant="outline" + onClick={() => router.push("/dashboard/scans")} + className="flex items-center gap-2" + > + <BarChart3 className="w-4 h-4" /> + View Reports + </Button> + <Button + onClick={() => router.push("/dashboard/websites/new")} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + Add Website + </Button> + </div> + </div> + </CardContent> + </Card> + </motion.div> + </div> + </DashboardLayout> + ); +} diff --git a/website-monitoring-frontend/src/app/dashboard/performance/page.tsx b/website-monitoring-frontend/src/app/dashboard/performance/page.tsx new file mode 100644 index 0000000..c7d65da --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/performance/page.tsx @@ -0,0 +1,400 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/layout/Card"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Zap, + TrendingUp, + TrendingDown, + Clock, + Target, + AlertTriangle, + CheckCircle, + BarChart3, + Loader2, +} from "lucide-react"; +import { useAuth } from "@/contexts/AuthContext"; +import { supabase } from "@/lib/supabase"; +import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils"; + +interface PerformanceMetric { + id: string; + website_name: string; + website_url: string; + lighthouse_score: number; + performance_score: number; + accessibility_score: number; + best_practices_score: number; + seo_score: number; + first_contentful_paint: number; + largest_contentful_paint: number; + cumulative_layout_shift: number; + total_blocking_time: number; + created_at: string; +} + +interface PerformanceSummary { + totalWebsites: number; + averageScore: number; + goodPerformance: number; + needsImprovement: number; + poor: number; +} + +export default function PerformancePage() { + const { userDetails } = useAuth(); + const [metrics, setMetrics] = useState<PerformanceMetric[]>([]); + const [summary, setSummary] = useState<PerformanceSummary>({ + totalWebsites: 0, + averageScore: 0, + goodPerformance: 0, + needsImprovement: 0, + poor: 0, + }); + const [loading, setLoading] = useState(true); + const [timeRange, setTimeRange] = useState<"7d" | "30d" | "90d">("30d"); + + useEffect(() => { + if (userDetails?.organization_id) { + loadPerformanceData(); + } + }, [userDetails, timeRange]); + + const loadPerformanceData = async () => { + if (!userDetails?.organization_id) return; + + try { + setLoading(true); + + // Calculate date range + const days = timeRange === "7d" ? 7 : timeRange === "30d" ? 30 : 90; + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + // Fetch latest performance data for each website + const { data: scanData, error } = await supabase + .from("scans") + .select(` + id, + lighthouse_score, + created_at, + scan_results!inner ( + category, + score, + metrics + ), + pages!inner ( + websites!inner ( + id, + name, + base_url, + organization_id + ) + ) + `) + .eq("pages.websites.organization_id", userDetails.organization_id) + .eq("status", "completed") + .gte("created_at", startDate.toISOString()) + .order("created_at", { ascending: false }); + + if (error) { + const errorInfo = extractSupabaseErrorInfo(error); + logError("Error loading performance data", error, { + organizationId: userDetails.organization_id, + timeRange, + startDate: startDate.toISOString(), + supabaseError: errorInfo + }); + + // If tables don't exist, set empty metrics + if (errorInfo.message?.includes("does not exist") || errorInfo.details?.includes("does not exist")) { + setMetrics([]); + setSummary({ + totalWebsites: 0, + averageScore: 0, + goodPerformance: 0, + needsImprovement: 0, + poor: 0, + }); + return; + } + throw error; + } + + // Process the data to get latest metrics per website + const websiteMetrics = new Map<string, PerformanceMetric>(); + + scanData?.forEach((scan: any) => { + const website = scan.pages.websites; + if (!websiteMetrics.has(website.id)) { + const results = scan.scan_results || []; + + websiteMetrics.set(website.id, { + id: scan.id, + website_name: website.name, + website_url: website.base_url, + lighthouse_score: scan.lighthouse_score || 0, + performance_score: results.find((r: any) => r.category === "performance")?.score || 0, + accessibility_score: results.find((r: any) => r.category === "accessibility")?.score || 0, + best_practices_score: results.find((r: any) => r.category === "best-practices")?.score || 0, + seo_score: results.find((r: any) => r.category === "seo")?.score || 0, + first_contentful_paint: results.find((r: any) => r.category === "performance")?.metrics?.first_contentful_paint || 0, + largest_contentful_paint: results.find((r: any) => r.category === "performance")?.metrics?.largest_contentful_paint || 0, + cumulative_layout_shift: results.find((r: any) => r.category === "performance")?.metrics?.cumulative_layout_shift || 0, + total_blocking_time: results.find((r: any) => r.category === "performance")?.metrics?.total_blocking_time || 0, + created_at: scan.created_at, + }); + } + }); + + const metricsArray = Array.from(websiteMetrics.values()); + setMetrics(metricsArray); + + // Calculate summary + if (metricsArray.length > 0) { + const avgScore = Math.round( + metricsArray.reduce((sum, m) => sum + m.lighthouse_score, 0) / metricsArray.length + ); + const good = metricsArray.filter(m => m.lighthouse_score >= 90).length; + const needsImprovement = metricsArray.filter(m => m.lighthouse_score >= 50 && m.lighthouse_score < 90).length; + const poor = metricsArray.filter(m => m.lighthouse_score < 50).length; + + setSummary({ + totalWebsites: metricsArray.length, + averageScore: avgScore, + goodPerformance: good, + needsImprovement, + poor, + }); + } + } catch (error) { + const errorInfo = extractSupabaseErrorInfo(error); + logError("Error loading performance data", error, { + organizationId: userDetails.organization_id, + timeRange, + function: "loadPerformanceData", + supabaseError: errorInfo + }); + } finally { + setLoading(false); + } + }; + + const getScoreColor = (score: number) => { + if (score >= 90) return "text-green-600 bg-green-100"; + if (score >= 50) return "text-yellow-600 bg-yellow-100"; + return "text-red-600 bg-red-100"; + }; + + const getScoreIcon = (score: number) => { + if (score >= 90) return <CheckCircle className="w-4 h-4" />; + if (score >= 50) return <AlertTriangle className="w-4 h-4" />; + return <TrendingDown className="w-4 h-4" />; + }; + + if (loading) { + return ( + <DashboardLayout> + <div className="flex items-center justify-center py-12"> + <Loader2 className="w-8 h-8 animate-spin text-blue-600" /> + </div> + </DashboardLayout> + ); + } + + return ( + <DashboardLayout> + <div className="space-y-6"> + {/* Header */} + <div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between"> + <div> + <h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2"> + <Zap className="w-6 h-6" /> + Performance Overview + </h1> + <p className="text-gray-600 mt-1"> + Monitor and analyze your websites' performance metrics + </p> + </div> + <select + value={timeRange} + onChange={(e) => setTimeRange(e.target.value as "7d" | "30d" | "90d")} + className="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + > + <option value="7d">Last 7 days</option> + <option value="30d">Last 30 days</option> + <option value="90d">Last 90 days</option> + </select> + </div> + + {/* Summary Cards */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Total Websites</p> + <p className="text-2xl font-bold">{summary.totalWebsites}</p> + </div> + <BarChart3 className="w-8 h-8 text-blue-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Average Score</p> + <p className="text-2xl font-bold">{summary.averageScore}</p> + </div> + <Target className="w-8 h-8 text-purple-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Good Performance</p> + <p className="text-2xl font-bold text-green-600">{summary.goodPerformance}</p> + </div> + <CheckCircle className="w-8 h-8 text-green-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Needs Improvement</p> + <p className="text-2xl font-bold text-yellow-600">{summary.needsImprovement}</p> + </div> + <AlertTriangle className="w-8 h-8 text-yellow-600" /> + </div> + </CardContent> + </Card> + </div> + + {/* Performance Metrics */} + <div className="space-y-4"> + <h2 className="text-lg font-semibold text-gray-900">Website Performance</h2> + + {metrics.length > 0 ? ( + <div className="grid gap-4"> + {metrics.map((metric, index) => ( + <motion.div + key={metric.id} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: index * 0.1 }} + > + <Card> + <CardContent className="p-6"> + <div className="flex items-start justify-between mb-4"> + <div> + <h3 className="font-semibold text-gray-900">{metric.website_name}</h3> + <p className="text-sm text-gray-500">{metric.website_url}</p> + <p className="text-xs text-gray-400"> + Last scan: {new Date(metric.created_at).toLocaleDateString()} + </p> + </div> + <Badge className={`flex items-center gap-1 ${getScoreColor(metric.lighthouse_score)}`}> + {getScoreIcon(metric.lighthouse_score)} + {metric.lighthouse_score}/100 + </Badge> + </div> + + <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> + <div className="text-center"> + <div className={`text-lg font-semibold ${getScoreColor(metric.performance_score).split(' ')[0]}`}> + {metric.performance_score} + </div> + <div className="text-xs text-gray-500">Performance</div> + </div> + <div className="text-center"> + <div className={`text-lg font-semibold ${getScoreColor(metric.accessibility_score).split(' ')[0]}`}> + {metric.accessibility_score} + </div> + <div className="text-xs text-gray-500">Accessibility</div> + </div> + <div className="text-center"> + <div className={`text-lg font-semibold ${getScoreColor(metric.best_practices_score).split(' ')[0]}`}> + {metric.best_practices_score} + </div> + <div className="text-xs text-gray-500">Best Practices</div> + </div> + <div className="text-center"> + <div className={`text-lg font-semibold ${getScoreColor(metric.seo_score).split(' ')[0]}`}> + {metric.seo_score} + </div> + <div className="text-xs text-gray-500">SEO</div> + </div> + </div> + + {(metric.first_contentful_paint > 0 || metric.largest_contentful_paint > 0) && ( + <div className="mt-4 pt-4 border-t border-gray-200"> + <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> + {metric.first_contentful_paint > 0 && ( + <div> + <div className="font-medium text-gray-900"> + {(metric.first_contentful_paint / 1000).toFixed(1)}s + </div> + <div className="text-gray-500">FCP</div> + </div> + )} + {metric.largest_contentful_paint > 0 && ( + <div> + <div className="font-medium text-gray-900"> + {(metric.largest_contentful_paint / 1000).toFixed(1)}s + </div> + <div className="text-gray-500">LCP</div> + </div> + )} + {metric.cumulative_layout_shift > 0 && ( + <div> + <div className="font-medium text-gray-900"> + {metric.cumulative_layout_shift.toFixed(3)} + </div> + <div className="text-gray-500">CLS</div> + </div> + )} + {metric.total_blocking_time > 0 && ( + <div> + <div className="font-medium text-gray-900"> + {metric.total_blocking_time}ms + </div> + <div className="text-gray-500">TBT</div> + </div> + )} + </div> + </div> + )} + </CardContent> + </Card> + </motion.div> + ))} + </div> + ) : ( + <Card> + <CardContent className="p-12 text-center"> + <Zap className="w-16 h-16 mx-auto text-gray-400 mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + No performance data available + </h3> + <p className="text-gray-600"> + Run scans on your websites to see performance metrics here + </p> + </CardContent> + </Card> + )} + </div> + </div> + </DashboardLayout> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/dashboard/seo/page.tsx b/website-monitoring-frontend/src/app/dashboard/seo/page.tsx new file mode 100644 index 0000000..dd6ee14 --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/seo/page.tsx @@ -0,0 +1,410 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/layout/Card"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Search, + TrendingUp, + TrendingDown, + AlertTriangle, + CheckCircle, + Target, + FileText, + Link, + Image, + Loader2, +} from "lucide-react"; +import { useAuth } from "@/contexts/AuthContext"; +import { supabase } from "@/lib/supabase"; +import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils"; + +interface SEOMetric { + id: string; + website_name: string; + website_url: string; + seo_score: number; + title_tag: boolean; + meta_description: boolean; + h1_tag: boolean; + image_alt_text: number; + internal_links: number; + external_links: number; + page_speed_score: number; + mobile_friendly: boolean; + ssl_certificate: boolean; + created_at: string; +} + +interface SEOSummary { + totalPages: number; + averageSEOScore: number; + goodSEO: number; + needsImprovement: number; + poor: number; +} + +export default function SEOPage() { + const { userDetails } = useAuth(); + const [metrics, setMetrics] = useState<SEOMetric[]>([]); + const [summary, setSummary] = useState<SEOSummary>({ + totalPages: 0, + averageSEOScore: 0, + goodSEO: 0, + needsImprovement: 0, + poor: 0, + }); + const [loading, setLoading] = useState(true); + const [timeRange, setTimeRange] = useState<"7d" | "30d" | "90d">("30d"); + + useEffect(() => { + if (userDetails?.organization_id) { + loadSEOData(); + } + }, [userDetails, timeRange]); + + const loadSEOData = async () => { + if (!userDetails?.organization_id) return; + + try { + setLoading(true); + + const days = timeRange === "7d" ? 7 : timeRange === "30d" ? 30 : 90; + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + // Fetch latest SEO data + const { data: scanData, error } = await supabase + .from("scans") + .select(` + id, + lighthouse_score, + created_at, + scan_results!inner ( + category, + score, + details + ), + pages!inner ( + id, + url, + websites!inner ( + id, + name, + base_url, + organization_id + ) + ) + `) + .eq("pages.websites.organization_id", userDetails.organization_id) + .eq("status", "completed") + .gte("created_at", startDate.toISOString()) + .order("created_at", { ascending: false }); + + if (error) throw error; + + // Process SEO data + const pageMetrics = new Map<string, SEOMetric>(); + + scanData?.forEach((scan: any) => { + const page = scan.pages; + const website = page.websites; + const seoResult = scan.scan_results?.find((r: any) => r.category === "seo"); + const performanceResult = scan.scan_results?.find((r: any) => r.category === "performance"); + + if (!pageMetrics.has(page.id) && seoResult) { + const details = seoResult.details || {}; + + pageMetrics.set(page.id, { + id: scan.id, + website_name: website.name, + website_url: page.url || website.base_url, + seo_score: seoResult.score || 0, + title_tag: details.has_title_tag || false, + meta_description: details.has_meta_description || false, + h1_tag: details.has_h1_tag || false, + image_alt_text: details.images_with_alt || 0, + internal_links: details.internal_links || 0, + external_links: details.external_links || 0, + page_speed_score: performanceResult?.score || 0, + mobile_friendly: details.mobile_friendly || false, + ssl_certificate: website.base_url?.startsWith("https://") || false, + created_at: scan.created_at, + }); + } + }); + + const metricsArray = Array.from(pageMetrics.values()); + setMetrics(metricsArray); + + // Calculate summary + if (metricsArray.length > 0) { + const avgScore = Math.round( + metricsArray.reduce((sum, m) => sum + m.seo_score, 0) / metricsArray.length + ); + const good = metricsArray.filter(m => m.seo_score >= 90).length; + const needsImprovement = metricsArray.filter(m => m.seo_score >= 50 && m.seo_score < 90).length; + const poor = metricsArray.filter(m => m.seo_score < 50).length; + + setSummary({ + totalPages: metricsArray.length, + averageSEOScore: avgScore, + goodSEO: good, + needsImprovement, + poor, + }); + } + } catch (error) { + const errorInfo = extractSupabaseErrorInfo(error); + logError("Error loading SEO data", error, { + organizationId: userDetails.organization_id, + timeRange, + function: "loadSEOData", + supabaseError: errorInfo + }); + } finally { + setLoading(false); + } + }; + + const getScoreColor = (score: number) => { + if (score >= 90) return "text-green-600 bg-green-100"; + if (score >= 50) return "text-yellow-600 bg-yellow-100"; + return "text-red-600 bg-red-100"; + }; + + const getScoreIcon = (score: number) => { + if (score >= 90) return <CheckCircle className="w-4 h-4" />; + if (score >= 50) return <AlertTriangle className="w-4 h-4" />; + return <TrendingDown className="w-4 h-4" />; + }; + + if (loading) { + return ( + <DashboardLayout> + <div className="flex items-center justify-center py-12"> + <Loader2 className="w-8 h-8 animate-spin text-blue-600" /> + </div> + </DashboardLayout> + ); + } + + return ( + <DashboardLayout> + <div className="space-y-6"> + {/* Header */} + <div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between"> + <div> + <h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2"> + <Search className="w-6 h-6" /> + SEO Analysis + </h1> + <p className="text-gray-600 mt-1"> + Monitor and optimize your websites' search engine optimization + </p> + </div> + <select + value={timeRange} + onChange={(e) => setTimeRange(e.target.value as "7d" | "30d" | "90d")} + className="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + > + <option value="7d">Last 7 days</option> + <option value="30d">Last 30 days</option> + <option value="90d">Last 90 days</option> + </select> + </div> + + {/* Summary Cards */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Total Pages</p> + <p className="text-2xl font-bold">{summary.totalPages}</p> + </div> + <FileText className="w-8 h-8 text-blue-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Average SEO Score</p> + <p className="text-2xl font-bold">{summary.averageSEOScore}</p> + </div> + <Target className="w-8 h-8 text-purple-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Good SEO</p> + <p className="text-2xl font-bold text-green-600">{summary.goodSEO}</p> + </div> + <CheckCircle className="w-8 h-8 text-green-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Needs Improvement</p> + <p className="text-2xl font-bold text-yellow-600">{summary.needsImprovement}</p> + </div> + <AlertTriangle className="w-8 h-8 text-yellow-600" /> + </div> + </CardContent> + </Card> + </div> + + {/* SEO Metrics */} + <div className="space-y-4"> + <h2 className="text-lg font-semibold text-gray-900">Page SEO Analysis</h2> + + {metrics.length > 0 ? ( + <div className="grid gap-4"> + {metrics.map((metric, index) => ( + <motion.div + key={metric.id} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: index * 0.1 }} + > + <Card> + <CardContent className="p-6"> + <div className="flex items-start justify-between mb-4"> + <div className="flex-1"> + <h3 className="font-semibold text-gray-900">{metric.website_name}</h3> + <p className="text-sm text-gray-500 truncate">{metric.website_url}</p> + <p className="text-xs text-gray-400"> + Last scan: {new Date(metric.created_at).toLocaleDateString()} + </p> + </div> + <Badge className={`flex items-center gap-1 ${getScoreColor(metric.seo_score)}`}> + {getScoreIcon(metric.seo_score)} + {metric.seo_score}/100 + </Badge> + </div> + + {/* SEO Checklist */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> + <div className="flex items-center gap-2"> + {metric.title_tag ? ( + <CheckCircle className="w-4 h-4 text-green-500" /> + ) : ( + <AlertTriangle className="w-4 h-4 text-red-500" /> + )} + <span className="text-sm">Title Tag</span> + </div> + + <div className="flex items-center gap-2"> + {metric.meta_description ? ( + <CheckCircle className="w-4 h-4 text-green-500" /> + ) : ( + <AlertTriangle className="w-4 h-4 text-red-500" /> + )} + <span className="text-sm">Meta Description</span> + </div> + + <div className="flex items-center gap-2"> + {metric.h1_tag ? ( + <CheckCircle className="w-4 h-4 text-green-500" /> + ) : ( + <AlertTriangle className="w-4 h-4 text-red-500" /> + )} + <span className="text-sm">H1 Tag</span> + </div> + + <div className="flex items-center gap-2"> + {metric.ssl_certificate ? ( + <CheckCircle className="w-4 h-4 text-green-500" /> + ) : ( + <AlertTriangle className="w-4 h-4 text-red-500" /> + )} + <span className="text-sm">SSL Certificate</span> + </div> + + <div className="flex items-center gap-2"> + {metric.mobile_friendly ? ( + <CheckCircle className="w-4 h-4 text-green-500" /> + ) : ( + <AlertTriangle className="w-4 h-4 text-red-500" /> + )} + <span className="text-sm">Mobile Friendly</span> + </div> + + <div className="flex items-center gap-2"> + <Image className="w-4 h-4 text-gray-500" /> + <span className="text-sm">{metric.image_alt_text} Images with Alt</span> + </div> + </div> + + {/* Links and Performance */} + <div className="mt-4 pt-4 border-t border-gray-200"> + <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> + <div className="flex items-center gap-2"> + <Link className="w-4 h-4 text-blue-500" /> + <div> + <div className="font-medium">{metric.internal_links}</div> + <div className="text-gray-500">Internal Links</div> + </div> + </div> + + <div className="flex items-center gap-2"> + <Link className="w-4 h-4 text-green-500" /> + <div> + <div className="font-medium">{metric.external_links}</div> + <div className="text-gray-500">External Links</div> + </div> + </div> + + <div className="flex items-center gap-2"> + <Target className="w-4 h-4 text-purple-500" /> + <div> + <div className="font-medium">{metric.page_speed_score}</div> + <div className="text-gray-500">Speed Score</div> + </div> + </div> + + <div className="flex items-center gap-2"> + <TrendingUp className="w-4 h-4 text-indigo-500" /> + <div> + <div className="font-medium">{metric.seo_score}</div> + <div className="text-gray-500">SEO Score</div> + </div> + </div> + </div> + </div> + </CardContent> + </Card> + </motion.div> + ))} + </div> + ) : ( + <Card> + <CardContent className="p-12 text-center"> + <Search className="w-16 h-16 mx-auto text-gray-400 mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + No SEO data available + </h3> + <p className="text-gray-600"> + Run scans on your websites to see SEO analysis here + </p> + </CardContent> + </Card> + )} + </div> + </div> + </DashboardLayout> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/dashboard/settings/page.tsx b/website-monitoring-frontend/src/app/dashboard/settings/page.tsx new file mode 100644 index 0000000..7c524ac --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/settings/page.tsx @@ -0,0 +1,576 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Settings, + User, + Bell, + Shield, + CreditCard, + Key, + Mail, + Smartphone, + Globe, + Database, + Zap, + Check, + X, + Loader2, +} from "lucide-react"; +import { useAuth } from "@/contexts/AuthContext"; +import { supabase } from "@/lib/supabase"; + +interface UserSettings { + email_notifications: boolean; + sms_notifications: boolean; + browser_notifications: boolean; + weekly_report: boolean; + timezone: string; + date_format: string; +} + +interface OrganizationSettings { + name: string; + subscription_tier: string; + subscription_status: string; + max_websites: number; + max_scans_per_month: number; + api_key: string; +} + +export default function SettingsPage() { + const { user, userDetails } = useAuth(); + const [activeTab, setActiveTab] = useState<"profile" | "notifications" | "organization" | "billing" | "api">("profile"); + const [userSettings, setUserSettings] = useState<UserSettings>({ + email_notifications: true, + sms_notifications: false, + browser_notifications: true, + weekly_report: true, + timezone: "UTC", + date_format: "MM/DD/YYYY", + }); + const [orgSettings, setOrgSettings] = useState<OrganizationSettings | null>(null); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [success, setSuccess] = useState(""); + const [error, setError] = useState(""); + + useEffect(() => { + loadSettings(); + }, [userDetails]); + + const loadSettings = async () => { + if (!userDetails?.organization_id) return; + + try { + setLoading(true); + + // Load organization settings + const { data: orgData, error: orgError } = await supabase + .from("organizations") + .select("*") + .eq("id", userDetails.organization_id) + .single(); + + if (orgError) throw orgError; + + if (orgData) { + setOrgSettings({ + name: orgData.name, + subscription_tier: orgData.subscription_tier, + subscription_status: orgData.subscription_status, + max_websites: orgData.max_websites || 10, + max_scans_per_month: orgData.max_scans_per_month || 1000, + api_key: orgData.api_key || "sk-" + Math.random().toString(36).substring(2, 15), + }); + } + + // Load user notification preferences (if they exist) + const { data: notificationData } = await supabase + .from("user_notification_preferences") + .select("*") + .eq("user_id", user?.id) + .single(); + + if (notificationData) { + setUserSettings({ + email_notifications: notificationData.email_notifications, + sms_notifications: notificationData.sms_notifications, + browser_notifications: notificationData.browser_notifications, + weekly_report: notificationData.weekly_report, + timezone: notificationData.timezone || "UTC", + date_format: notificationData.date_format || "MM/DD/YYYY", + }); + } + + } catch (error) { + console.error("Error loading settings:", error); + } finally { + setLoading(false); + } + }; + + const saveUserSettings = async () => { + if (!user?.id) return; + + try { + setSaving(true); + setError(""); + + const { error } = await supabase + .from("user_notification_preferences") + .upsert({ + user_id: user.id, + ...userSettings, + }); + + if (error) throw error; + + setSuccess("Settings saved successfully"); + setTimeout(() => setSuccess(""), 3000); + + } catch (error) { + console.error("Error saving settings:", error); + setError("Failed to save settings"); + } finally { + setSaving(false); + } + }; + + const saveOrgSettings = async () => { + if (!userDetails?.organization_id || !orgSettings) return; + + try { + setSaving(true); + setError(""); + + const { error } = await supabase + .from("organizations") + .update({ + name: orgSettings.name, + }) + .eq("id", userDetails.organization_id); + + if (error) throw error; + + setSuccess("Organization settings saved successfully"); + setTimeout(() => setSuccess(""), 3000); + + } catch (error) { + console.error("Error saving organization settings:", error); + setError("Failed to save organization settings"); + } finally { + setSaving(false); + } + }; + + const generateNewApiKey = async () => { + if (!userDetails?.organization_id) return; + + try { + setSaving(true); + const newApiKey = "sk-" + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + + const { error } = await supabase + .from("organizations") + .update({ api_key: newApiKey }) + .eq("id", userDetails.organization_id); + + if (error) throw error; + + setOrgSettings(prev => prev ? { ...prev, api_key: newApiKey } : null); + setSuccess("New API key generated successfully"); + setTimeout(() => setSuccess(""), 3000); + + } catch (error) { + console.error("Error generating API key:", error); + setError("Failed to generate new API key"); + } finally { + setSaving(false); + } + }; + + const tabs = [ + { id: "profile", label: "Profile", icon: User }, + { id: "notifications", label: "Notifications", icon: Bell }, + { id: "organization", label: "Organization", icon: Globe }, + { id: "billing", label: "Billing", icon: CreditCard }, + { id: "api", label: "API", icon: Key }, + ]; + + return ( + <DashboardLayout> + <div className="space-y-6"> + {/* Header */} + <div> + <h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2"> + <Settings className="w-6 h-6" /> + Settings + </h1> + <p className="text-gray-600 mt-1"> + Manage your account and organization preferences + </p> + </div> + + {/* Success/Error Messages */} + {success && ( + <div className="bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-3"> + <Check className="w-5 h-5 text-green-500" /> + <span className="text-green-800">{success}</span> + </div> + )} + + {error && ( + <div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3"> + <X className="w-5 h-5 text-red-500" /> + <span className="text-red-800">{error}</span> + </div> + )} + + {/* Tabs */} + <div className="border-b border-gray-200"> + <nav className="-mb-px flex space-x-8"> + {tabs.map((tab) => { + const Icon = tab.icon; + return ( + <button + key={tab.id} + onClick={() => setActiveTab(tab.id as any)} + className={`flex items-center gap-2 py-2 px-1 border-b-2 font-medium text-sm ${ + activeTab === tab.id + ? "border-blue-500 text-blue-600" + : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" + }`} + > + <Icon className="w-4 h-4" /> + {tab.label} + </button> + ); + })} + </nav> + </div> + + {/* Tab Content */} + <div className="space-y-6"> + {activeTab === "profile" && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <User className="w-5 h-5" /> + Profile Information + </CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Full Name + </label> + <input + type="text" + value={userDetails?.name || ""} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + readOnly + /> + </div> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Email Address + </label> + <input + type="email" + value={user?.email || ""} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + readOnly + /> + </div> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Role + </label> + <Badge className="bg-blue-100 text-blue-800"> + {userDetails?.role?.toUpperCase()} + </Badge> + </div> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Member Since + </label> + <span className="text-gray-600"> + — + </span> + </div> + </div> + </CardContent> + </Card> + )} + + {activeTab === "notifications" && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Bell className="w-5 h-5" /> + Notification Preferences + </CardTitle> + </CardHeader> + <CardContent className="space-y-6"> + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-3"> + <Mail className="w-5 h-5 text-gray-500" /> + <div> + <p className="font-medium">Email Notifications</p> + <p className="text-sm text-gray-500">Receive alerts and updates via email</p> + </div> + </div> + <label className="relative inline-flex items-center cursor-pointer"> + <input + type="checkbox" + checked={userSettings.email_notifications} + onChange={(e) => setUserSettings(prev => ({ ...prev, email_notifications: e.target.checked }))} + className="sr-only peer" + /> + <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> + </label> + </div> + + <div className="flex items-center justify-between"> + <div className="flex items-center gap-3"> + <Smartphone className="w-5 h-5 text-gray-500" /> + <div> + <p className="font-medium">SMS Notifications</p> + <p className="text-sm text-gray-500">Receive urgent alerts via SMS</p> + </div> + </div> + <label className="relative inline-flex items-center cursor-pointer"> + <input + type="checkbox" + checked={userSettings.sms_notifications} + onChange={(e) => setUserSettings(prev => ({ ...prev, sms_notifications: e.target.checked }))} + className="sr-only peer" + /> + <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> + </label> + </div> + + <div className="flex items-center justify-between"> + <div className="flex items-center gap-3"> + <Bell className="w-5 h-5 text-gray-500" /> + <div> + <p className="font-medium">Browser Notifications</p> + <p className="text-sm text-gray-500">Show desktop notifications in your browser</p> + </div> + </div> + <label className="relative inline-flex items-center cursor-pointer"> + <input + type="checkbox" + checked={userSettings.browser_notifications} + onChange={(e) => setUserSettings(prev => ({ ...prev, browser_notifications: e.target.checked }))} + className="sr-only peer" + /> + <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> + </label> + </div> + + <div className="flex items-center justify-between"> + <div className="flex items-center gap-3"> + <Database className="w-5 h-5 text-gray-500" /> + <div> + <p className="font-medium">Weekly Reports</p> + <p className="text-sm text-gray-500">Receive weekly performance summaries</p> + </div> + </div> + <label className="relative inline-flex items-center cursor-pointer"> + <input + type="checkbox" + checked={userSettings.weekly_report} + onChange={(e) => setUserSettings(prev => ({ ...prev, weekly_report: e.target.checked }))} + className="sr-only peer" + /> + <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> + </label> + </div> + </div> + + <div className="pt-4 border-t"> + <Button + onClick={saveUserSettings} + disabled={saving} + className="flex items-center gap-2" + > + {saving ? ( + <Loader2 className="w-4 h-4 animate-spin" /> + ) : ( + <Check className="w-4 h-4" /> + )} + Save Preferences + </Button> + </div> + </CardContent> + </Card> + )} + + {activeTab === "organization" && orgSettings && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Globe className="w-5 h-5" /> + Organization Settings + </CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Organization Name + </label> + <input + type="text" + value={orgSettings.name} + onChange={(e) => setOrgSettings({ ...orgSettings, name: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + </div> + + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Subscription Plan + </label> + <Badge className="bg-purple-100 text-purple-800"> + {orgSettings.subscription_tier.toUpperCase()} + </Badge> + </div> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Status + </label> + <Badge className={orgSettings.subscription_status === "active" ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"}> + {orgSettings.subscription_status.toUpperCase()} + </Badge> + </div> + </div> + + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Max Websites + </label> + <span className="text-gray-600">{orgSettings.max_websites}</span> + </div> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Max Scans per Month + </label> + <span className="text-gray-600">{orgSettings.max_scans_per_month.toLocaleString()}</span> + </div> + </div> + + <div className="pt-4 border-t"> + <Button + onClick={saveOrgSettings} + disabled={saving} + className="flex items-center gap-2" + > + {saving ? ( + <Loader2 className="w-4 h-4 animate-spin" /> + ) : ( + <Check className="w-4 h-4" /> + )} + Save Changes + </Button> + </div> + </CardContent> + </Card> + )} + + {activeTab === "billing" && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <CreditCard className="w-5 h-5" /> + Billing & Subscription + </CardTitle> + </CardHeader> + <CardContent> + <div className="text-center py-12"> + <CreditCard className="w-16 h-16 mx-auto text-gray-400 mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + Billing Management + </h3> + <p className="text-gray-600 mb-6"> + Billing features are not yet implemented in this demo + </p> + <Button variant="outline"> + Contact Support + </Button> + </div> + </CardContent> + </Card> + )} + + {activeTab === "api" && orgSettings && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Key className="w-5 h-5" /> + API Configuration + </CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + API Key + </label> + <div className="flex gap-2"> + <input + type="text" + value={orgSettings.api_key} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm" + readOnly + /> + <Button + variant="outline" + onClick={generateNewApiKey} + disabled={saving} + > + {saving ? ( + <Loader2 className="w-4 h-4 animate-spin" /> + ) : ( + "Regenerate" + )} + </Button> + </div> + <p className="text-sm text-gray-500 mt-1"> + Use this API key to authenticate requests to our API + </p> + </div> + + <div className="bg-gray-50 p-4 rounded-lg"> + <h4 className="font-medium text-gray-900 mb-2">API Endpoints</h4> + <div className="space-y-2 text-sm"> + <div className="flex justify-between"> + <code className="text-blue-600">GET /api/websites</code> + <span className="text-gray-500">List websites</span> + </div> + <div className="flex justify-between"> + <code className="text-blue-600">POST /api/websites/{"{id}"}/scan</code> + <span className="text-gray-500">Trigger scan</span> + </div> + <div className="flex justify-between"> + <code className="text-blue-600">GET /api/scans/{"{id}"}</code> + <span className="text-gray-500">Get scan results</span> + </div> + </div> + </div> + </CardContent> + </Card> + )} + </div> + </div> + </DashboardLayout> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/dashboard/team/page.tsx b/website-monitoring-frontend/src/app/dashboard/team/page.tsx new file mode 100644 index 0000000..8703326 --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/team/page.tsx @@ -0,0 +1,398 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { motion, AnimatePresence } from "framer-motion"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Users, + Plus, + Mail, + Settings, + Trash2, + Crown, + Shield, + User, + MoreVertical, + Check, + X, + Loader2, +} from "lucide-react"; +import { useAuth } from "@/contexts/AuthContext"; +import { supabase } from "@/lib/supabase"; + +interface TeamMember { + id: string; + email: string; + name: string; + role: "owner" | "admin" | "member"; + status: "active" | "pending"; + created_at: string; + last_login_at?: string; +} + +export default function TeamPage() { + const router = useRouter(); + const { userDetails, user } = useAuth(); + const [members, setMembers] = useState<TeamMember[]>([]); + const [loading, setLoading] = useState(true); + const [inviting, setInviting] = useState(false); + const [inviteEmail, setInviteEmail] = useState(""); + const [inviteRole, setInviteRole] = useState<"admin" | "member">("member"); + const [success, setSuccess] = useState(""); + const [error, setError] = useState(""); + + useEffect(() => { + if (userDetails?.organization_id) { + loadTeamMembers(); + } + }, [userDetails]); + + const loadTeamMembers = async () => { + if (!userDetails?.organization_id) return; + + try { + setLoading(true); + const { data, error } = await supabase + .from("users") + .select("*") + .eq("organization_id", userDetails.organization_id) + .order("created_at", { ascending: false }); + + if (error) { + console.error("Error loading team members:", error); + // If it's a missing table error, set empty array + if (error.message?.includes("does not exist")) { + setMembers([]); + return; + } + throw error; + } + setMembers(data || []); + } catch (error) { + console.error("Error loading team members:", error); + setError("Failed to load team members"); + } finally { + setLoading(false); + } + }; + + const handleInviteMember = async (e: React.FormEvent) => { + e.preventDefault(); + if (!inviteEmail.trim() || !userDetails?.organization_id) return; + + try { + setInviting(true); + setError(""); + + // Check if user already exists + const { data: existingUser } = await supabase + .from("users") + .select("id") + .eq("email", inviteEmail.toLowerCase()) + .single(); + + if (existingUser) { + setError("User is already a member of an organization"); + return; + } + + // Send invitation (in a real app, you'd send an email) + // For now, we'll create a pending user record + const { error: inviteError } = await supabase + .from("team_invitations") + .insert([ + { + email: inviteEmail.toLowerCase(), + role: inviteRole, + organization_id: userDetails.organization_id, + invited_by: user?.id, + status: "pending", + }, + ]); + + if (inviteError) throw inviteError; + + setSuccess(`Invitation sent to ${inviteEmail}`); + setInviteEmail(""); + setInviteRole("member"); + await loadTeamMembers(); + } catch (error) { + console.error("Error inviting member:", error); + setError("Failed to send invitation"); + } finally { + setInviting(false); + } + }; + + const handleRemoveMember = async (memberId: string) => { + if (!confirm("Are you sure you want to remove this team member?")) return; + + try { + const { error } = await supabase + .from("users") + .delete() + .eq("id", memberId); + + if (error) throw error; + + setSuccess("Team member removed successfully"); + await loadTeamMembers(); + } catch (error) { + console.error("Error removing member:", error); + setError("Failed to remove team member"); + } + }; + + const handleUpdateRole = async (memberId: string, newRole: string) => { + try { + const { error } = await supabase + .from("users") + .update({ role: newRole }) + .eq("id", memberId); + + if (error) throw error; + + setSuccess("Member role updated successfully"); + await loadTeamMembers(); + } catch (error) { + console.error("Error updating role:", error); + setError("Failed to update member role"); + } + }; + + const getRoleIcon = (role: string) => { + switch (role) { + case "owner": + return <Crown className="w-4 h-4 text-yellow-500" />; + case "admin": + return <Shield className="w-4 h-4 text-blue-500" />; + default: + return <User className="w-4 h-4 text-gray-500" />; + } + }; + + const getRoleBadgeColor = (role: string) => { + switch (role) { + case "owner": + return "bg-yellow-100 text-yellow-800"; + case "admin": + return "bg-blue-100 text-blue-800"; + default: + return "bg-gray-100 text-gray-800"; + } + }; + + const canManageMembers = userDetails?.role === "owner" || userDetails?.role === "admin"; + + if (loading) { + return ( + <DashboardLayout> + <div className="flex items-center justify-center py-12"> + <Loader2 className="w-8 h-8 animate-spin text-blue-600" /> + </div> + </DashboardLayout> + ); + } + + return ( + <DashboardLayout> + <div className="space-y-6"> + {/* Success/Error Messages */} + <AnimatePresence> + {success && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + className="bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-3" + > + <Check className="w-5 h-5 text-green-500" /> + <span className="text-green-800">{success}</span> + <Button + variant="outline" + size="sm" + onClick={() => setSuccess("")} + className="ml-auto" + > + Dismiss + </Button> + </motion.div> + )} + + {error && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3" + > + <X className="w-5 h-5 text-red-500" /> + <span className="text-red-800">{error}</span> + <Button + variant="outline" + size="sm" + onClick={() => setError("")} + className="ml-auto" + > + Dismiss + </Button> + </motion.div> + )} + </AnimatePresence> + + {/* Header */} + <div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between"> + <div> + <h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2"> + <Users className="w-6 h-6" /> + Team Members ({members.length}) + </h1> + <p className="text-gray-600 mt-1"> + Manage your organization's team members and permissions + </p> + </div> + {canManageMembers && ( + <Button + onClick={() => document.getElementById("invite-form")?.scrollIntoView()} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + Invite Member + </Button> + )} + </div> + + {/* Invite Form */} + {canManageMembers && ( + <Card id="invite-form"> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Mail className="w-5 h-5" /> + Invite New Member + </CardTitle> + </CardHeader> + <CardContent> + <form onSubmit={handleInviteMember} className="flex gap-4"> + <input + type="email" + placeholder="Enter email address" + value={inviteEmail} + onChange={(e) => setInviteEmail(e.target.value)} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + required + /> + <select + value={inviteRole} + onChange={(e) => setInviteRole(e.target.value as "admin" | "member")} + className="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + > + <option value="member">Member</option> + <option value="admin">Admin</option> + </select> + <Button type="submit" disabled={inviting}> + {inviting ? ( + <Loader2 className="w-4 h-4 animate-spin" /> + ) : ( + "Send Invite" + )} + </Button> + </form> + </CardContent> + </Card> + )} + + {/* Team Members List */} + <div className="grid gap-4"> + {members.map((member) => ( + <motion.div + key={member.id} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + className="group" + > + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-4"> + <div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center"> + <User className="w-5 h-5 text-gray-600" /> + </div> + <div> + <h3 className="font-semibold text-gray-900">{member.name}</h3> + <p className="text-sm text-gray-500">{member.email}</p> + {member.last_login_at && ( + <p className="text-xs text-gray-400"> + Last login: {new Date(member.last_login_at).toLocaleDateString()} + </p> + )} + </div> + </div> + + <div className="flex items-center gap-3"> + <Badge className={`flex items-center gap-1 ${getRoleBadgeColor(member.role)}`}> + {getRoleIcon(member.role)} + {member.role.charAt(0).toUpperCase() + member.role.slice(1)} + </Badge> + + {canManageMembers && member.id !== user?.id && ( + <div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"> + {member.role !== "owner" && ( + <select + value={member.role} + onChange={(e) => handleUpdateRole(member.id, e.target.value)} + className="text-sm px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" + > + <option value="member">Member</option> + <option value="admin">Admin</option> + </select> + )} + {member.role !== "owner" && ( + <Button + variant="outline" + size="sm" + onClick={() => handleRemoveMember(member.id)} + className="text-red-600 hover:text-red-700 hover:bg-red-50" + > + <Trash2 className="w-3 h-3" /> + </Button> + )} + </div> + )} + </div> + </div> + </CardContent> + </Card> + </motion.div> + ))} + </div> + + {members.length === 0 && ( + <Card> + <CardContent className="p-12 text-center"> + <Users className="w-16 h-16 mx-auto text-gray-400 mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + No team members yet + </h3> + <p className="text-gray-600 mb-6"> + Start building your team by inviting members to your organization + </p> + {canManageMembers && ( + <Button + onClick={() => document.getElementById("invite-form")?.scrollIntoView()} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + Invite Your First Member + </Button> + )} + </CardContent> + </Card> + )} + </div> + </DashboardLayout> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/app/dashboard/websites/[id]/page.tsx b/website-monitoring-frontend/src/app/dashboard/websites/[id]/page.tsx new file mode 100644 index 0000000..b8c98d6 --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/websites/[id]/page.tsx @@ -0,0 +1,485 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { WebsiteSettings } from "@/components/dashboard/WebsiteSettings"; +import { CrawlerControl } from "@/components/dashboard/CrawlerControl"; +import { CrawlDebugger } from "@/components/debug/CrawlDebugger"; +import { Button } from "@/components/ui/forms/Button"; +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Trash2, + Globe, + Calendar, + Activity, + FileText, + Search, + AlertCircle, + CheckCircle, + Clock, + ExternalLink, + Settings, + Play, + Bug, +} from "lucide-react"; +import { useRouter } from "next/navigation"; +import { supabase } from "@/lib/supabase"; +import { websiteService } from "@/services/websiteService"; +import { ScanScheduleManager } from '@/components/dashboard/ScanScheduleManager'; + +interface WebsiteData { + id: string; + name: string; + base_url: string; + is_active: boolean; + created_at: string; + organization_id: string; + stats: { + pagesCount: number; + scansCount: number; + latestScan: { + id: string; + status: string; + created_at: string; + } | null; + }; +} + +// Custom hook to handle async params +function useAsyncParams<T>(params: Promise<T> | T): T | null { + const [resolvedParams, setResolvedParams] = useState<T | null>(null); + + useEffect(() => { + const resolveParams = async () => { + try { + const resolved = await Promise.resolve(params); + setResolvedParams(resolved); + } catch (error) { + console.error("Failed to resolve params:", error); + } + }; + resolveParams(); + }, [params]); + + return resolvedParams; +} + +export default function WebsiteDetailsPage(props: any) { + // Handle async params properly for Next.js 15+ + const [websiteId, setWebsiteId] = useState<string | null>(null); + + useEffect(() => { + const resolveParams = async () => { + try { + const params = await Promise.resolve(props?.params); + setWebsiteId(params?.id || null); + } catch (error) { + console.error("Failed to resolve params:", error); + } + }; + resolveParams(); + }, [props?.params]); + const [website, setWebsite] = useState<WebsiteData | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleteConfirmText, setDeleteConfirmText] = useState(""); + const [deleting, setDeleting] = useState(false); + const [activeSection, setActiveSection] = useState("overview"); + const router = useRouter(); + + const loadWebsiteData = useCallback(async () => { + if (!websiteId) return; + try { + setLoading(true); + const data = await websiteService.getWebsite(websiteId); + setWebsite(data as WebsiteData); + } catch (err: unknown) { + setError( + err instanceof Error ? err.message : "Failed to load website data", + ); + } finally { + setLoading(false); + } + }, [websiteId]); + + useEffect(() => { + loadWebsiteData(); + }, [loadWebsiteData]); + + const handleDelete = async () => { + if (!websiteId) return; + setDeleting(true); + try { + const { error } = await supabase + .from("websites") + .delete() + .eq("id", websiteId); + if (error) { + alert("Failed to delete website: " + error.message); + } else { + router.push("/dashboard/websites"); + } + } catch (err: unknown) { + alert( + "Failed to delete website: " + + (err instanceof Error ? err.message : String(err)), + ); + } finally { + setDeleting(false); + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; + + const getStatusBadge = (status: string) => { + const statusConfig = { + completed: { color: "green", icon: CheckCircle }, + running: { color: "blue", icon: Clock }, + failed: { color: "red", icon: AlertCircle }, + pending: { color: "yellow", icon: Clock }, + }; + + const config = + statusConfig[status as keyof typeof statusConfig] || statusConfig.pending; + const Icon = config.icon; + + return ( + <Badge + variant={config.color as "green" | "blue" | "red" | "yellow"} + className="flex items-center gap-1" + > + <Icon className="w-3 h-3" /> + {status.charAt(0).toUpperCase() + status.slice(1)} + </Badge> + ); + }; + + if (loading || !websiteId) { + return ( + <DashboardLayout> + <div className="flex items-center justify-center min-h-96"> + <div className="text-center"> + <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div> + <p>{!websiteId ? "Loading..." : "Loading website details..."}</p> + </div> + </div> + </DashboardLayout> + ); + } + + if (error || !website) { + return ( + <DashboardLayout> + <div className="flex items-center justify-center min-h-96"> + <div className="text-center"> + <AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" /> + <p className="text-red-600">{error || "Website not found"}</p> + <Button + onClick={() => router.push("/dashboard/websites")} + className="mt-4" + > + Back to Websites + </Button> + </div> + </div> + </DashboardLayout> + ); + } + + return ( + <DashboardLayout> + <div className="max-w-7xl mx-auto space-y-6"> + {/* Header Section */} + <div className="flex items-start justify-between"> + <div className="flex items-center gap-4"> + <img + src={`https://www.google.com/s2/favicons?domain=${encodeURIComponent(website.base_url)}`} + alt="Website favicon" + className="w-12 h-12 rounded-lg border shadow-sm" + /> + <div> + <h1 className="text-3xl font-bold text-gray-900"> + {website.name} + </h1> + <div className="flex items-center gap-2 mt-1"> + <Globe className="w-4 h-4 text-gray-500" /> + <a + href={website.base_url} + target="_blank" + rel="noopener noreferrer" + className="text-blue-600 hover:text-blue-800 flex items-center gap-1" + > + {website.base_url} + <ExternalLink className="w-3 h-3" /> + </a> + </div> + </div> + </div> + <Badge variant={website.is_active ? "green" : "gray"}> + {website.is_active ? "Active" : "Inactive"} + </Badge> + </div> + + {/* Navigation Tabs */} + <div className="border-b border-gray-200"> + <nav className="-mb-px flex space-x-8"> + {[ + { id: "overview", label: "Overview", icon: Activity }, + { id: "crawler", label: "Crawler Control", icon: Play }, + { id: "debug", label: "Debug", icon: Bug }, + { id: "settings", label: "Settings", icon: Settings }, + { id: "danger", label: "Danger Zone", icon: Trash2 }, + ].map((tab) => { + const Icon = tab.icon; + return ( + <button + key={tab.id} + onClick={() => setActiveSection(tab.id)} + className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 ${ + activeSection === tab.id + ? "border-blue-500 text-blue-600" + : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" + }`} + > + <Icon className="w-4 h-4" /> + {tab.label} + </button> + ); + })} + </nav> + </div> + + {/* Content Sections */} + {activeSection === "overview" && ( + <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> + {/* Statistics Cards */} + <div className="lg:col-span-2 grid grid-cols-1 md:grid-cols-3 gap-4"> + <Card> + <CardContent className="p-6"> + <div className="flex items-center"> + <div className="flex-1"> + <p className="text-sm font-medium text-gray-600"> + Total Pages + </p> + <p className="text-2xl font-bold text-gray-900"> + {website.stats.pagesCount} + </p> + </div> + <FileText className="w-8 h-8 text-blue-500" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center"> + <div className="flex-1"> + <p className="text-sm font-medium text-gray-600"> + Total Scans + </p> + <p className="text-2xl font-bold text-gray-900"> + {website.stats.scansCount} + </p> + </div> + <Search className="w-8 h-8 text-green-500" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center"> + <div className="flex-1"> + <p className="text-sm font-medium text-gray-600"> + Status + </p> + <div className="mt-1"> + {website.stats.latestScan ? ( + getStatusBadge(website.stats.latestScan.status) + ) : ( + <Badge variant="gray">No scans</Badge> + )} + </div> + </div> + <Activity className="w-8 h-8 text-purple-500" /> + </div> + </CardContent> + </Card> + </div> + + {/* Website Information */} + <Card> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4"> + Website Information + </h3> + <div className="space-y-3"> + <div> + <p className="text-sm text-gray-600">Created</p> + <p className="font-medium flex items-center gap-2"> + <Calendar className="w-4 h-4" /> + {formatDate(website.created_at)} + </p> + </div> + + {website.stats.latestScan && ( + <div> + <p className="text-sm text-gray-600">Last Scan</p> + <p className="font-medium flex items-center gap-2"> + <Clock className="w-4 h-4" /> + {formatDate(website.stats.latestScan.created_at)} + </p> + </div> + )} + + <div> + <p className="text-sm text-gray-600">Website ID</p> + <p className="font-mono text-sm bg-gray-100 p-2 rounded"> + {website.id} + </p> + </div> + </div> + </CardContent> + </Card> + + {/* Recent Activity */} + <Card className="lg:col-span-3"> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4">Recent Activity</h3> + {website.stats.latestScan ? ( + <div className="border-l-4 border-blue-500 pl-4"> + <div className="flex items-center justify-between"> + <div> + <p className="font-medium">Latest Scan Completed</p> + <p className="text-sm text-gray-600"> + Scan ID: {website.stats.latestScan.id} + </p> + </div> + <div className="text-right"> + {getStatusBadge(website.stats.latestScan.status)} + <p className="text-sm text-gray-500 mt-1"> + {formatDate(website.stats.latestScan.created_at)} + </p> + </div> + </div> + </div> + ) : ( + <p className="text-gray-500 italic">No recent activity</p> + )} + </CardContent> + </Card> + </div> + )} + + {activeSection === "crawler" && websiteId && ( + <div className="max-w-4xl mx-auto"> + <Card> + <CardContent className="p-6"> + <h2 className="text-2xl font-bold mb-6"> + Crawler Control Panel + </h2> + <CrawlerControl websiteId={websiteId} /> + </CardContent> + </Card> + </div> + )} + + {activeSection === "debug" && websiteId && ( + <div className="max-w-6xl mx-auto"> + <CrawlDebugger websiteId={websiteId} /> + </div> + )} + + {activeSection === "settings" && websiteId && ( + <div className="max-w-4xl mx-auto"> + <WebsiteSettings websiteId={websiteId} /> + </div> + )} + + {activeSection === "danger" && websiteId && ( + <div className="max-w-4xl mx-auto"> + <Card className="border-red-200"> + <CardContent className="p-6"> + <h2 className="text-2xl font-bold text-red-700 mb-6"> + Danger Zone + </h2> + <div className="bg-red-50 border border-red-200 p-6 rounded-lg"> + <h3 className="text-lg font-semibold text-red-800 mb-3"> + Delete Website + </h3> + <p className="text-sm text-red-700 mb-4"> + Once you delete a website, there is no going back. This will + permanently delete the website and all associated data + including scans, pages, and analytics. + </p> + <Button + variant="destructive" + onClick={() => setShowDeleteConfirm(true)} + className="flex items-center gap-2" + > + <Trash2 className="w-4 h-4" /> + Delete Website + </Button> + </div> + </CardContent> + </Card> + </div> + )} + + {/* Delete Confirmation Modal */} + {showDeleteConfirm && ( + <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> + <div className="bg-white rounded-lg shadow-lg p-6 max-w-md w-full mx-4"> + <h3 className="text-lg font-semibold mb-2">Delete Website</h3> + <p className="text-sm text-gray-700 mb-2"> + To{" "} + <span className="font-bold text-red-600"> + permanently delete + </span>{" "} + <span className="font-bold">{website.name}</span> and{" "} + <span className="font-bold">all its data</span>, type{" "} + <span className="font-bold">DELETE</span> below and confirm. + </p> + <input + className="border rounded px-3 py-2 w-full mb-4" + placeholder="Type DELETE to confirm" + value={deleteConfirmText} + onChange={(e) => setDeleteConfirmText(e.target.value)} + autoFocus + /> + <div className="flex justify-end gap-3"> + <Button + variant="outline" + onClick={() => { + setShowDeleteConfirm(false); + setDeleteConfirmText(""); + }} + disabled={deleting} + > + Cancel + </Button> + <Button + variant="destructive" + onClick={handleDelete} + disabled={deleting || deleteConfirmText !== "DELETE"} + > + {deleting ? "Deleting..." : "Delete Website"} + </Button> + </div> + </div> + </div> + )} + </div> + </DashboardLayout> + ); +} diff --git a/website-monitoring-frontend/src/app/dashboard/websites/[id]/settings/page.tsx b/website-monitoring-frontend/src/app/dashboard/websites/[id]/settings/page.tsx new file mode 100644 index 0000000..254d790 --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/websites/[id]/settings/page.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { WebsiteSettings } from "@/components/dashboard/WebsiteSettings"; + +export default function WebsiteSettingsPage(props: any) { + const [id, setId] = useState<string | null>(null); + + useEffect(() => { + const resolveParams = async () => { + try { + const params = await Promise.resolve(props?.params); + setId(params?.id || null); + } catch (error) { + console.error("Failed to resolve params:", error); + } + }; + resolveParams(); + }, [props?.params]); + + return ( + <DashboardLayout> + {id ? <WebsiteSettings websiteId={id} /> : <div>Loading...</div>} + </DashboardLayout> + ); +} diff --git a/website-monitoring-frontend/src/app/dashboard/websites/new/page.tsx b/website-monitoring-frontend/src/app/dashboard/websites/new/page.tsx new file mode 100644 index 0000000..a75a909 --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/websites/new/page.tsx @@ -0,0 +1,12 @@ +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { AddWebsiteForm } from "@/components/dashboard/AddWebsiteForm"; + +export default function AddWebsitePage() { + return ( + <DashboardLayout> + <div className="py-6"> + <AddWebsiteForm /> + </div> + </DashboardLayout> + ); +} diff --git a/website-monitoring-frontend/src/app/dashboard/websites/page.tsx b/website-monitoring-frontend/src/app/dashboard/websites/page.tsx new file mode 100644 index 0000000..981194d --- /dev/null +++ b/website-monitoring-frontend/src/app/dashboard/websites/page.tsx @@ -0,0 +1,13 @@ +import { DashboardLayout } from "@/components/layouts/DashboardLayout"; +import { EnhancedWebsiteList } from "@/components/dashboard/EnhancedWebsiteList"; +import { ScanScheduleManager } from '@/components/dashboard/ScanScheduleManager'; + +export default function WebsitesPage() { + return ( + <DashboardLayout> + <div className="py-6"> + <EnhancedWebsiteList /> + </div> + </DashboardLayout> + ); +} diff --git a/website-monitoring-frontend/src/app/globals.css b/website-monitoring-frontend/src/app/globals.css new file mode 100644 index 0000000..ebf685f --- /dev/null +++ b/website-monitoring-frontend/src/app/globals.css @@ -0,0 +1,298 @@ +@import "tailwindcss"; + +@plugin "tailwindcss-animate"; + +@custom-variant dark (&:is(.dark *)); + +@theme { + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.129 0.042 264.695); + --card: oklch(1 0 0); + --card-foreground: oklch(0.129 0.042 264.695); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.129 0.042 264.695); + --primary: oklch(0.208 0.042 265.755); + --primary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.968 0.007 247.896); + --secondary-foreground: oklch(0.208 0.042 265.755); + --muted: oklch(0.968 0.007 247.896); + --muted-foreground: oklch(0.554 0.046 257.417); + --accent: oklch(0.968 0.007 247.896); + --accent-foreground: oklch(0.208 0.042 265.755); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.929 0.013 255.508); + --input: oklch(0.929 0.013 255.508); + --ring: oklch(0.704 0.04 256.788); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.984 0.003 247.858); + --sidebar-foreground: oklch(0.129 0.042 264.695); + --sidebar-primary: oklch(0.208 0.042 265.755); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.968 0.007 247.896); + --sidebar-accent-foreground: oklch(0.208 0.042 265.755); + --sidebar-border: oklch(0.929 0.013 255.508); + --sidebar-ring: oklch(0.704 0.04 256.788); +} + +.dark { + --background: oklch(0.129 0.042 264.695); + --foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.129 0.042 264.695); + --card-foreground: oklch(0.984 0.003 247.858); + --popover: oklch(0.129 0.042 264.695); + --popover-foreground: oklch(0.984 0.003 247.858); + --primary: oklch(0.984 0.003 247.858); + --primary-foreground: oklch(0.208 0.042 265.755); + --secondary: oklch(0.279 0.041 260.031); + --secondary-foreground: oklch(0.984 0.003 247.858); + --muted: oklch(0.279 0.041 260.031); + --muted-foreground: oklch(0.704 0.04 256.788); + --accent: oklch(0.279 0.041 260.031); + --accent-foreground: oklch(0.984 0.003 247.858); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.279 0.041 260.031); + --input: oklch(0.279 0.041 260.031); + --ring: oklch(0.446 0.043 257.281); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.208 0.042 265.755); + --sidebar-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.279 0.041 260.031); + --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --sidebar-border: oklch(0.279 0.041 260.031); + --sidebar-ring: oklch(0.446 0.043 257.281); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-slideDown { + animation: slideDown 0.3s ease-out; +} + +@keyframes progress { + 0% { + width: 0%; + } + 50% { + width: 70%; + } + 100% { + width: 100%; + } +} + +.animate-progress { + animation: progress 3s ease-in-out infinite; +} +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fadeIn { + animation: fadeIn 0.3s ease-out; +} + +.animate-slideDown { + animation: slideDown 0.3s ease-out; +} + +/* Responsive styles */ +@media (max-width: 768px) { + .grid-cols-2 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + + .grid-cols-4 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .max-w-7xl { + max-width: 100%; + } + + .p-8 { + padding: 2rem; + } + + .text-4xl { + font-size: 2rem; + } + + .text-2xl { + font-size: 1.5rem; + } + + .text-lg { + font-size: 1.125rem; + } + + .text-sm { + font-size: 0.875rem; + } + + .h-24 { + height: 6rem; + } + + .w-24 { + width: 6rem; + } + + .h-2 { + height: 0.5rem; + } + + .p-6 { + padding: 1.5rem; + } + + .p-4 { + padding: 1rem; + } + + .p-3 { + padding: 0.75rem; + } + + .p-2 { + padding: 0.5rem; + } + + .p-1 { + padding: 0.25rem; + } + + .space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); + } + + .space-y-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1rem * var(--tw-space-y-reverse)); + } + + .space-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); + } +} + +@keyframes fadeInUp { + 0% { + opacity: 0; + transform: translateY(30px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} +.animate-fadeInUp { + animation: fadeInUp 0.8s ease forwards; +} + +@layer utilities { + .bg-grid-pattern { + background-image: linear-gradient( + to right, + #e5e7eb 1px, + transparent 1px + ), + linear-gradient(to bottom, #e5e7eb 1px, transparent 1px); + background-size: 24px 24px; + } +} diff --git a/website-monitoring-frontend/src/app/layout.tsx b/website-monitoring-frontend/src/app/layout.tsx new file mode 100644 index 0000000..00ff745 --- /dev/null +++ b/website-monitoring-frontend/src/app/layout.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import { Providers } from "./providers"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Website Monitoring", + description: + "Analyze and optimize your website's performance, accessibility, and SEO", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + <html lang="en"> + <body + className={`${geistSans.variable} ${geistMono.variable} antialiased`} + > + <Providers>{children} </Providers> + </body> + </html> + ); +} diff --git a/website-monitoring-frontend/src/app/page.tsx b/website-monitoring-frontend/src/app/page.tsx new file mode 100644 index 0000000..43c2854 --- /dev/null +++ b/website-monitoring-frontend/src/app/page.tsx @@ -0,0 +1,5 @@ +import HeroPage from "@/components/demo/HeroPage"; + +export default function Home() { + return <HeroPage />; +} diff --git a/website-monitoring-frontend/src/app/providers.tsx b/website-monitoring-frontend/src/app/providers.tsx new file mode 100644 index 0000000..b45d029 --- /dev/null +++ b/website-monitoring-frontend/src/app/providers.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { AuthProvider } from "@/contexts/AuthContext"; +import { ErrorBoundary } from "@/components/ui/feedback/ErrorBoundary"; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + <AuthProvider> + <ErrorBoundary>{children}</ErrorBoundary> + </AuthProvider> + ); +} diff --git a/website-monitoring-frontend/src/components/CrawlerResultsSelector.tsx b/website-monitoring-frontend/src/components/CrawlerResultsSelector.tsx new file mode 100644 index 0000000..8eb0760 --- /dev/null +++ b/website-monitoring-frontend/src/components/CrawlerResultsSelector.tsx @@ -0,0 +1,236 @@ +import { useState, useEffect } from "react"; +import { supabase } from "@/lib/supabase"; +import { Button } from "@/components/ui/forms/Button"; +import { Checkbox } from "@/components/ui/forms/Checkbox"; +import { AlertCircle, CheckCircle, Globe } from "lucide-react"; + +export function CrawlerResultsSelector({ + sessionId, + websiteId, + onSelectionChange, +}: { + sessionId: string; + websiteId: string; + onSelectionChange?: (selected: string[]) => void; +}) { + const [pages, setPages] = useState< + Array<{ id: string; url: string; title: string; is_active: boolean }> + >([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState<string | null>(null); + const [success, setSuccess] = useState(false); + + // Load the pages for this session + useEffect(() => { + loadPagesForSession(); + }, [sessionId]); + + const loadPagesForSession = async () => { + try { + setLoading(true); + setError(null); + + // Method 1: Get pages that have this crawl session ID in their metadata + const { data: sessionPages, error: sessionPagesError } = await supabase + .from("pages") + .select("id, url, title, is_active, metadata") + .eq("website_id", websiteId) + .contains("metadata", { crawl_session_id: sessionId }); + console.log("[CrawlerResultsSelector] Method 1 - sessionPages:", sessionPages); + + let pagesData = sessionPages || []; + // Ensure all pages have a metadata property (for type safety) + pagesData = pagesData.map((p) => ({ ...p, metadata: p.metadata ?? {} })); + + // Method 2: If no pages found with session metadata, try discovered URLs from session + if (pagesData.length === 0) { + const { data: session, error: sessionError } = await supabase + .from("crawl_sessions") + .select("metadata") + .eq("id", sessionId) + .single(); + console.log("[CrawlerResultsSelector] Method 2 - crawl_sessions.metadata:", session?.metadata); + + if (!sessionError && session?.metadata?.discovered_urls) { + const discoveredUrls = session.metadata.discovered_urls; + + const { data, error } = await supabase + .from("pages") + .select("id, url, title, is_active") + .eq("website_id", websiteId) + .in("url", discoveredUrls); + console.log("[CrawlerResultsSelector] Method 2 - pages by discovered_urls:", data); + + if (!error) { + pagesData = (data || []).map((p) => ({ ...p, metadata: {} })); + } + } + } + + // Method 3: Fallback to all pages for this website if still empty + if (pagesData.length === 0) { + const { data, error } = await supabase + .from("pages") + .select("id, url, title, is_active") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }); // Remove .limit(50) to show all + console.log("[CrawlerResultsSelector] Method 3 - all pages:", data); + + if (!error) { + pagesData = (data || []).map((p) => ({ ...p, metadata: {} })); + } + } + + setPages(pagesData); + } catch (err) { + console.error("Error loading pages:", err); + setError("Failed to load pages. Please try again."); + } finally { + setLoading(false); + } + }; + + const togglePageSelection = (pageId: string) => { + setPages((currentPages) => + currentPages.map((page) => + page.id === pageId ? { ...page, is_active: !page.is_active } : page, + ), + ); + }; + + const handleSaveSelection = async () => { + try { + setSaving(true); + setError(null); + setSuccess(false); + + // Prepare updates for all pages (active and inactive) + const updates = pages.map((page) => ({ + id: page.id, + is_active: page.is_active, + })); + + // Update each page individually since upsert might not work as expected + for (const update of updates) { + const { error } = await supabase + .from("pages") + .update({ is_active: update.is_active }) + .eq("id", update.id); + + if (error) throw error; + } + + // Call the callback with selected URLs if provided + if (onSelectionChange) { + const selectedUrls = pages + .filter((page) => page.is_active) + .map((page) => page.url); + onSelectionChange(selectedUrls); + } + + setSuccess(true); + setTimeout(() => setSuccess(false), 3000); + } catch (err) { + console.error("Error saving page selection:", err); + setError("Failed to save your selection. Please try again."); + } finally { + setSaving(false); + } + }; + + const selectAll = () => { + setPages((currentPages) => + currentPages.map((page) => ({ ...page, is_active: true })), + ); + }; + + const deselectAll = () => { + setPages((currentPages) => + currentPages.map((page) => ({ ...page, is_active: false })), + ); + }; + + if (loading) { + return <div className="p-4 text-center">Loading discovered pages...</div>; + } + + if (pages.length === 0) { + return ( + <div className="p-4 text-center"> + <Globe className="h-12 w-12 mx-auto text-gray-400 mb-2" /> + <p>No pages have been discovered yet.</p> + <p className="text-sm text-gray-500 mt-2"> + Wait for the crawl to complete, then refresh this view.<br /> + <span className="text-xs text-red-500">(If you believe this is an error, check your crawl session and page metadata in Supabase. The UI now always falls back to showing all pages for this website.)</span> + </p> + </div> + ); + } + + return ( + <div className="space-y-4"> + <div className="flex justify-between items-center mb-4"> + <h3 className="text-lg font-semibold"> + Discovered Pages ({pages.length}) + <span className="text-sm text-gray-500 ml-2"> + ({pages.filter((p) => p.is_active).length} selected) + </span> + </h3> + <div className="flex gap-2"> + <Button variant="outline" size="sm" onClick={selectAll}> + Select All + </Button> + <Button variant="outline" size="sm" onClick={deselectAll}> + Deselect All + </Button> + </div> + </div> + + {error && ( + <div className="p-3 bg-red-50 text-red-700 rounded-md flex items-center"> + <AlertCircle className="h-5 w-5 mr-2" /> + <span>{error}</span> + </div> + )} + + {success && ( + <div className="p-3 bg-green-50 text-green-700 rounded-md flex items-center"> + <CheckCircle className="h-5 w-5 mr-2" /> + <span>Page selection saved successfully!</span> + </div> + )} + + <div className="max-h-[400px] overflow-y-auto border rounded-md"> + <ul className="divide-y"> + {pages.map((page) => ( + <li key={page.id} className="p-3 hover:bg-gray-50"> + <div className="flex items-center"> + <Checkbox + id={`page-${page.id}`} + checked={page.is_active} + onCheckedChange={() => togglePageSelection(page.id)} + className="mr-3" + /> + <div className="flex-1 overflow-hidden"> + <p className="font-medium truncate"> + {page.title || "Untitled Page"} + </p> + <p className="text-sm text-gray-500 truncate">{page.url}</p> + </div> + </div> + </li> + ))} + </ul> + </div> + + <Button + onClick={handleSaveSelection} + disabled={saving} + className="w-full" + > + {saving ? "Saving..." : "Save Selection"} + </Button> + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/WebsiteMonitoring.ts.backup b/website-monitoring-frontend/src/components/WebsiteMonitoring.ts.backup new file mode 100644 index 0000000..6b69898 --- /dev/null +++ b/website-monitoring-frontend/src/components/WebsiteMonitoring.ts.backup @@ -0,0 +1,50 @@ +'use client'; + +import { useState } from 'react'; +import { monitoringService } from '@/services/monitoringService'; +import { useAuth } from '@/contexts/AuthContext'; + +export function WebsiteMonitoring() { + const [url, setUrl] = useState(''); + const [loading, setLoading] = useState(false); + const { user } = useAuth(); + + const handleAddWebsite = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + const website = await monitoringService.addWebsite( + user?.organization_id, + url, + new URL(url).hostname + ); + + // Start analysis + await monitoringService.initiateAnalysis(website.id); + + setUrl(''); + // Refresh website list or show success message + } catch (error) { + console.error('Failed to add website:', error); + // Show error message + } finally { + setLoading(false); + } + }; + + return ( + <form onSubmit={handleAddWebsite}> + <input + type="url" + value={url} + onChange={(e) => setUrl(e.target.value)} + placeholder="Enter website URL" + required + /> + <button type="submit" disabled={loading}> + {loading ? 'Adding...' : 'Add Website'} + </button> + </form> + ); +} diff --git a/website-monitoring-frontend/src/components/WebsiteMonitoring.tsx b/website-monitoring-frontend/src/components/WebsiteMonitoring.tsx new file mode 100644 index 0000000..a87780a --- /dev/null +++ b/website-monitoring-frontend/src/components/WebsiteMonitoring.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { monitoringService } from '@/services/monitoringService'; +import { useAuth } from '@/contexts/AuthContext'; +import { supabase } from '@/lib/supabase'; + +export function WebsiteMonitoring() { + const [url, setUrl] = useState(''); + const [loading, setLoading] = useState(false); + const [organizationId, setOrganizationId] = useState<string | null>(null); + const { user } = useAuth(); + + useEffect(() => { + const getUserProfile = async () => { + if (user?.id) { + try { + const { data: profile, error } = await supabase + .from('users') + .select('organization_id') + .eq('id', user.id) + .single(); + + if (!error && profile) { + setOrganizationId(profile.organization_id); + } + } catch (error) { + console.error('Failed to get user profile:', error); + } + } + }; + + getUserProfile(); + }, [user?.id]); + + const handleAddWebsite = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + if (!organizationId) { + throw new Error('No organization found for user'); + } + + const website = await monitoringService.addWebsite( + organizationId, + url, + new URL(url).hostname + ); + + // Start analysis + await monitoringService.initiateAnalysis(website.id); + + setUrl(''); + // Refresh website list or show success message + } catch (error) { + console.error('Failed to add website:', error); + // Show error message + } finally { + setLoading(false); + } + }; + + return ( + <form onSubmit={handleAddWebsite}> + <input + type="url" + value={url} + onChange={(e) => setUrl(e.target.value)} + placeholder="Enter website URL" + required + /> + <button type="submit" disabled={loading}> + {loading ? 'Adding...' : 'Add Website'} + </button> + </form> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/components/auth/AuthForm.tsx b/website-monitoring-frontend/src/components/auth/AuthForm.tsx new file mode 100644 index 0000000..e217332 --- /dev/null +++ b/website-monitoring-frontend/src/components/auth/AuthForm.tsx @@ -0,0 +1,705 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { motion, AnimatePresence } from "framer-motion"; +import { useAuth } from "@/contexts/AuthContext"; +import { Button } from "@/components/ui/forms/Button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/forms/Form"; +import { Input } from "@/components/ui/forms/Input"; +import { supabase } from "@/lib/supabase"; +import { + Mail, + Key, + User, + Loader2, + ArrowRight, + Check, + RefreshCw, +} from "lucide-react"; +import { ErrorFeedback } from "@/components/ui/ErrorFeedback"; + +const emailSchema = z.object({ + email: z.string().email("Please enter a valid email address"), +}); + +const loginSchema = emailSchema.extend({ + password: z.string().min(6, "Password must be at least 6 characters"), +}); + +const registerSchema = loginSchema.extend({ + name: z.string().min(2, "Name must be at least 2 characters"), + password: z.string().min(6, "Password must be at least 6 characters"), +}); + +type FormState = "email" | "login" | "register" | "reset-password"; + +interface AuthFormProps { + initialEmail?: string | null; +} + +// Add this helper function above your AuthForm component +const getAuthErrorMessage = (error: unknown): string => { + // Check if it's an AuthApiError with a message + if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') { + // Handle specific error types + if (error.message.includes("Database error")) { + return "We're having trouble creating your account. This could be because the email is already registered or our systems are temporarily down."; + } else if (error.message.includes("User already registered")) { + return "This email is already registered. Please try logging in instead."; + } else if (error.message.includes("Invalid email")) { + return "Please provide a valid email address."; + } else if (error.message.includes("Password")) { + return "Your password doesn't meet the requirements. Please use at least 6 characters."; + } + return error.message; + } + + return "Registration failed. Please try again later."; +}; + +export function AuthForm({ initialEmail }: AuthFormProps) { + const [formState, setFormState] = useState<FormState>("email"); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [verificationSent, setVerificationSent] = useState(false); + const [resetEmailSent, setResetEmailSent] = useState(false); + const { signIn } = useAuth(); + const [email, setEmail] = useState(initialEmail || ""); + + useEffect(() => { + if (verificationSent) { + const timer = setTimeout(() => { + setVerificationSent(false); + }, 10000); + return () => clearTimeout(timer); + } + }, [verificationSent]); + + useEffect(() => { + if (resetEmailSent) { + const timer = setTimeout(() => { + setResetEmailSent(false); + }, 10000); + return () => clearTimeout(timer); + } + }, [resetEmailSent]); + + const emailForm = useForm<z.infer<typeof emailSchema>>({ + resolver: zodResolver(emailSchema), + defaultValues: { email: initialEmail || "" }, + }); + + const loginForm = useForm<z.infer<typeof loginSchema>>({ + resolver: zodResolver(loginSchema), + defaultValues: { email: "", password: "" }, + }); + + const registerForm = useForm<z.infer<typeof registerSchema>>({ + resolver: zodResolver(registerSchema), + defaultValues: { email: "", password: "", name: "" }, + }); + + const checkEmail = async (data: z.infer<typeof emailSchema>) => { + try { + setLoading(true); + setError(""); + setEmail(data.email); + + const res = await fetch('/api/email-exists', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: data.email }), + }); + const result = await res.json(); + + if (result.exists) { + setFormState("login"); + loginForm.setValue("email", data.email); + } else { + setFormState("register"); + registerForm.setValue("email", data.email); + } + } catch { + setFormState("register"); + registerForm.setValue("email", data.email); + } finally { + setLoading(false); + } + }; + + const handleLogin = async (data: z.infer<typeof loginSchema>) => { + try { + setLoading(true); + setError(""); + await signIn(data.email, data.password); + setSuccess(true); + } catch (error) { + setError(error instanceof Error ? error.message : "Invalid credentials"); + } finally { + setLoading(false); + } + }; + + const handleRegister = async (data: z.infer<typeof registerSchema>) => { + try { + setLoading(true); + setError(""); + + // Step 1: Create the organization + const { data: org, error: orgError } = await supabase + .from("organizations") + .insert([ + { + name: `${data.name}'s Organization`, + subscription_tier: "free", + subscription_status: "active", + }, + ]) + .select() + .single(); + + if (orgError) { + console.error("Organization creation error:", orgError); + console.error("Organization creation error details:", { + message: orgError.message, + details: orgError.details, + hint: orgError.hint, + code: orgError.code + }); + throw new Error(`Failed to create organization: ${orgError.message || 'Unknown error'}`); + } + + // Step 2: Sign up the user + const { data: authData, error: signUpError } = await supabase.auth.signUp( + { + email: data.email, + password: data.password, + options: { + emailRedirectTo: `${window.location.origin}/auth/callback`, + data: { + name: data.name, + organization_id: org.id, + role: "owner", + }, + }, + }, + ); + + if (signUpError) { + console.error("Sign-up error:", signUpError); + throw new Error(getAuthErrorMessage(signUpError)); + } + + if (!authData.user) { + throw new Error("User account creation failed"); + } + + // Step 3: User profile is automatically created by the handle_new_user trigger + // No need to manually create it here + + // Success! Email verification is required + setSuccess(true); + setVerificationSent(true); + } catch (error) { + console.error("Registration error:", error); + if (error instanceof Error) { + setError(error.message); + } else { + setError("Registration failed. Please try again later."); + } + } finally { + setLoading(false); + } + }; + + const handlePasswordReset = async (data: z.infer<typeof emailSchema>) => { + try { + setLoading(true); + setError(""); + + const { error } = await supabase.auth.resetPasswordForEmail(data.email, { + redirectTo: `${window.location.origin}/auth/reset-password`, + }); + + if (error) { + throw error; + } + + setResetEmailSent(true); + setSuccess(true); + } catch (error) { + console.error("Password reset error:", error); + if (error instanceof Error) { + setError(error.message); + } else { + setError("Failed to send reset email. Please try again."); + } + } finally { + setLoading(false); + } + }; + + const handleGoogleSignIn = async () => { + try { + setLoading(true); + setError(""); + + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${window.location.origin}/auth/callback`, + }, + }); + + if (error) { + throw error; + } + + // The redirect will happen automatically + } catch (error) { + console.error("Google sign-in error:", error); + if (error instanceof Error) { + setError(error.message); + } else { + setError("Failed to sign in with Google. Please try again."); + } + setLoading(false); + } + }; + + return ( + <div className="space-y-4"> + <AnimatePresence mode="sync" initial={false}> + {/* Email Form */} + {formState === "email" && ( + <motion.div + key="email-form" + initial={{ opacity: 0, x: -20 }} + animate={{ opacity: 1, x: 0 }} + exit={{ opacity: 0, x: 20 }} + transition={{ duration: 0.2 }} + > + <Form {...emailForm}> + <form + onSubmit={emailForm.handleSubmit(checkEmail)} + className="space-y-4" + > + <FormField + control={emailForm.control} + name="email" + render={({ field }) => ( + <FormItem> + <FormControl> + <div className="relative"> + <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" /> + <Input + {...field} + type="email" + placeholder="Enter your email" + className="pl-10" + disabled={loading} + /> + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <Button + type="submit" + disabled={loading} + className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 transition-all duration-300" + > + {loading ? ( + <Loader2 className="h-5 w-5 animate-spin" /> + ) : ( + <> + Continue + <ArrowRight className="ml-2 h-4 w-4" /> + </> + )} + </Button> + </form> + </Form> + </motion.div> + )} + + {/* Login Form */} + {formState === "login" && ( + <motion.div + key="login-form" + initial={{ opacity: 0, x: -20 }} + animate={{ opacity: 1, x: 0 }} + exit={{ opacity: 0, x: 20 }} + transition={{ duration: 0.2 }} + > + <Form {...loginForm}> + <form + onSubmit={loginForm.handleSubmit(handleLogin)} + className="space-y-4" + > + <div className="text-sm text-gray-600 flex items-center justify-between"> + <span> + Logging in as <span className="font-medium">{email}</span> + </span> + <button + type="button" + onClick={() => setFormState("email")} + className="text-blue-600 hover:underline text-sm" + > + Change + </button> + </div> + <FormField + control={loginForm.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormControl> + <div className="relative"> + <Key className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" /> + <Input + {...field} + type="password" + placeholder="Enter your password" + className="pl-10" + disabled={loading} + /> + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <Button + type="submit" + disabled={loading} + className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 transition-all duration-300" + > + {loading ? ( + <Loader2 className="h-5 w-5 animate-spin" /> + ) : success ? ( + <> + <Check className="h-5 w-5 mr-2" /> + Signed in + </> + ) : ( + "Sign in" + )} + </Button> + <div className="text-center"> + <button + type="button" + onClick={() => setFormState("reset-password")} + className="text-sm text-blue-600 hover:underline" + > + Forgot your password? + </button> + </div> + + {/* Divider */} + <div className="relative"> + <div className="absolute inset-0 flex items-center"> + <span className="w-full border-t border-gray-300" /> + </div> + <div className="relative flex justify-center text-sm"> + <span className="bg-white px-2 text-gray-500">Or continue with</span> + </div> + </div> + + {/* OAuth Buttons */} + <Button + type="button" + variant="outline" + onClick={handleGoogleSignIn} + disabled={loading} + className="w-full" + > + <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24"> + <path + fill="currentColor" + d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" + /> + <path + fill="currentColor" + d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" + /> + <path + fill="currentColor" + d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" + /> + <path + fill="currentColor" + d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" + /> + </svg> + Continue with Google + </Button> + </form> + </Form> + </motion.div> + )} + + {/* Register Form */} + {formState === "register" && ( + <motion.div + key="register-form" + initial={{ opacity: 0, x: -20 }} + animate={{ opacity: 1, x: 0 }} + exit={{ opacity: 0, x: 20 }} + transition={{ duration: 0.2 }} + > + <Form {...registerForm}> + <form + onSubmit={registerForm.handleSubmit(handleRegister)} + className="space-y-4" + > + <div className="text-sm text-gray-600 flex items-center justify-between"> + <span> + Creating account for{" "} + <span className="font-medium">{email}</span> + </span> + <button + type="button" + onClick={() => setFormState("email")} + className="text-blue-600 hover:underline text-sm" + > + Change + </button> + </div> + <FormField + control={registerForm.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormControl> + <div className="relative"> + <User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" /> + <Input + {...field} + placeholder="Enter your name" + className="pl-10" + disabled={loading} + /> + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={registerForm.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormControl> + <div className="relative"> + <Key className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" /> + <Input + {...field} + type="password" + placeholder="Create a password" + className="pl-10" + disabled={loading} + /> + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <Button + type="submit" + disabled={loading} + className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 transition-all duration-300" + > + {loading ? ( + <Loader2 className="h-5 w-5 animate-spin" /> + ) : success ? ( + <> + <Check className="h-5 w-5 mr-2" /> + Account created + </> + ) : ( + "Create account" + )} + </Button> + + {/* Divider */} + <div className="relative"> + <div className="absolute inset-0 flex items-center"> + <span className="w-full border-t border-gray-300" /> + </div> + <div className="relative flex justify-center text-sm"> + <span className="bg-white px-2 text-gray-500">Or continue with</span> + </div> + </div> + + {/* OAuth Buttons */} + <Button + type="button" + variant="outline" + onClick={handleGoogleSignIn} + disabled={loading} + className="w-full" + > + <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24"> + <path + fill="currentColor" + d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" + /> + <path + fill="currentColor" + d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" + /> + <path + fill="currentColor" + d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" + /> + <path + fill="currentColor" + d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" + /> + </svg> + Continue with Google + </Button> + </form> + </Form> + </motion.div> + )} + + {/* Reset Password Form */} + {formState === "reset-password" && ( + <motion.div + key="reset-form" + initial={{ opacity: 0, x: -20 }} + animate={{ opacity: 1, x: 0 }} + exit={{ opacity: 0, x: 20 }} + transition={{ duration: 0.2 }} + > + <Form {...emailForm}> + <form + onSubmit={emailForm.handleSubmit(handlePasswordReset)} + className="space-y-4" + > + <div className="text-sm text-gray-600 flex items-center justify-between"> + <span>Reset your password</span> + <button + type="button" + onClick={() => setFormState("email")} + className="text-blue-600 hover:underline text-sm" + > + Back to login + </button> + </div> + <div className="text-sm text-gray-500 mb-4"> + Enter your email address and we'll send you a link to reset your password. + </div> + <FormField + control={emailForm.control} + name="email" + render={({ field }) => ( + <FormItem> + <FormControl> + <div className="relative"> + <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" /> + <Input + {...field} + type="email" + placeholder="Enter your email" + className="pl-10" + disabled={loading} + /> + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <Button + type="submit" + disabled={loading} + className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 transition-all duration-300" + > + {loading ? ( + <Loader2 className="h-5 w-5 animate-spin" /> + ) : success ? ( + <> + <Check className="h-5 w-5 mr-2" /> + Reset email sent + </> + ) : ( + "Send reset email" + )} + </Button> + </form> + </Form> + </motion.div> + )} + </AnimatePresence> + + {/* Error and Verification Messages */} + <AnimatePresence mode="sync" initial={false}> + {error && ( + <ErrorFeedback + title="Registration Failed" + message={error} + details={ + error.includes("Database error") + ? "This might be temporary. You can try again or contact support if the problem persists." + : undefined + } + severity="error" + actions={[ + { + label: "Try Again", + onClick: () => setError(""), + icon: <RefreshCw className="h-4 w-4" />, + }, + { + label: "Contact Support", + onClick: () => + window.open( + "mailto:support@yourwebsite.com?subject=Registration%20Issue", + "_blank", + ), + icon: <Mail className="h-4 w-4" />, + }, + ]} + onDismiss={() => setError("")} + /> + )} + + {verificationSent && ( + <ErrorFeedback + title="Verification Email Sent" + message="Please check your email to verify your account." + details="The verification link will expire in 24 hours." + severity="info" + onDismiss={() => setVerificationSent(false)} + /> + )} + + {resetEmailSent && ( + <ErrorFeedback + title="Password Reset Email Sent" + message="Please check your email for password reset instructions." + details="The reset link will expire in 1 hour. If you don't see the email, check your spam folder." + severity="info" + onDismiss={() => setResetEmailSent(false)} + /> + )} + </AnimatePresence> + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/auth/FixAccount.tsx b/website-monitoring-frontend/src/components/auth/FixAccount.tsx new file mode 100644 index 0000000..02cd0ec --- /dev/null +++ b/website-monitoring-frontend/src/components/auth/FixAccount.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { useState } from "react"; +import { supabase } from "@/lib/supabase"; +import { Button } from "@/components/ui/forms/Button"; +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { AlertCircle, Check } from "lucide-react"; + +export function FixAccount() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + const [success, setSuccess] = useState(false); + const [debugInfo, setDebugInfo] = useState<any>(null); + + const fixAccount = async () => { + try { + setLoading(true); + setError(null); + setSuccess(false); + + // 1. Get current authenticated user + const { data: authData, error: authError } = + await supabase.auth.getUser(); + if (authError) throw new Error(`Auth error: ${authError.message}`); + if (!authData.user) throw new Error("No authenticated user found"); + + setDebugInfo((prev: any) => ({ ...prev, authUser: authData.user })); + const user = authData.user; + + // 2. Check if user already exists in users table + const { data: existingUser, error: userCheckError } = await supabase + .from("users") + .select("*") + .eq("id", user.id) + .maybeSingle(); + + setDebugInfo((prev: any) => ({ ...prev, existingUser })); + + if (userCheckError) + throw new Error(`Database error: ${userCheckError.message}`); + + // User already exists, just needs organization + if (existingUser) { + if (existingUser.organization_id) { + setDebugInfo((prev: any) => ({ + ...prev, + status: "User already has organization", + })); + setSuccess(true); + return; + } + } + + // 3. Create organization + const { data: org, error: orgError } = await supabase + .from("organizations") + .insert([ + { + name: `${user.email}'s Organization`, + subscription_tier: "free", + subscription_status: "active", + }, + ]) + .select() + .single(); + + if (orgError) + throw new Error(`Failed to create organization: ${orgError.message}`); + setDebugInfo((prev: any) => ({ ...prev, createdOrg: org })); + + // 4. Create user record if it doesn't exist + if (!existingUser) { + const { error: createUserError } = await supabase.from("users").insert([ + { + id: user.id, + email: user.email, + name: + user.user_metadata?.name || user.email?.split("@")[0] || "User", + organization_id: org.id, + role: "owner", + }, + ]); + + if (createUserError) + throw new Error(`Failed to create user: ${createUserError.message}`); + setDebugInfo((prev: any) => ({ + ...prev, + status: "Created new user record", + })); + } else { + // 5. Update existing user with organization + const { error: updateUserError } = await supabase + .from("users") + .update({ organization_id: org.id }) + .eq("id", user.id); + + if (updateUserError) + throw new Error(`Failed to update user: ${updateUserError.message}`); + setDebugInfo((prev: any) => ({ + ...prev, + status: "Updated existing user with organization", + })); + } + + setSuccess(true); + // Reload the page after success to refresh auth context + setTimeout(() => { + window.location.reload(); + }, 1500); + } catch (err) { + console.error("Error fixing account:", err); + setError( + err instanceof Error ? err.message : "An unknown error occurred", + ); + } finally { + setLoading(false); + } + }; + + return ( + <Card className="mx-auto max-w-md"> + <CardContent className="p-6"> + <h2 className="text-xl font-bold mb-4">Account Setup</h2> + <p className="mb-4 text-gray-600"> + Your account needs to be properly configured in the database. Click + the button below to fix your account setup. + </p> + + {error && ( + <div className="bg-red-50 text-red-700 p-4 rounded-md flex items-center mb-4"> + <AlertCircle className="h-5 w-5 mr-2" /> + <span>{error}</span> + </div> + )} + + {success && ( + <div className="bg-green-50 text-green-700 p-4 rounded-md flex items-center mb-4"> + <Check className="h-5 w-5 mr-2" /> + <span>Account successfully configured! Reloading page...</span> + </div> + )} + + <Button + onClick={fixAccount} + disabled={loading || success} + className="w-full" + > + {loading ? "Setting up account..." : "Fix My Account"} + </Button> + + {debugInfo && ( + <div className="mt-8 text-xs text-gray-500"> + <details> + <summary className="cursor-pointer">Debug Info</summary> + <pre className="mt-2 p-2 bg-gray-100 rounded overflow-auto max-h-60"> + {JSON.stringify(debugInfo, null, 2)} + </pre> + </details> + </div> + )} + </CardContent> + </Card> + ); +} diff --git a/website-monitoring-frontend/src/components/common/DashboardLayout.tsx b/website-monitoring-frontend/src/components/common/DashboardLayout.tsx new file mode 100644 index 0000000..5072cfe --- /dev/null +++ b/website-monitoring-frontend/src/components/common/DashboardLayout.tsx @@ -0,0 +1,11 @@ +export const DashboardLayout = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( + <div className="w-full bg-gray-50"> + <div className="w-full mx-auto">{children}</div> + </div> + ); +}; diff --git a/website-monitoring-frontend/src/components/core/competitorDashboard/CompetitorDashboardBase.tsx b/website-monitoring-frontend/src/components/core/competitorDashboard/CompetitorDashboardBase.tsx new file mode 100644 index 0000000..a6abbaf --- /dev/null +++ b/website-monitoring-frontend/src/components/core/competitorDashboard/CompetitorDashboardBase.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useState } from "react"; +import { MetricsOverview } from "@/components/core/competitorDashboard/MetricsOverview"; +import { DetailedMetrics } from "@/components/core/competitorDashboard/DetailedMetrics"; +import { Button } from "@/components/ui/forms/Button"; +import { Plus } from "lucide-react"; +import type { CompetitorData } from "@/types/metrics"; + +interface CompetitorDashboardBaseProps { + data: CompetitorData; + isDemo?: boolean; +} + +export const CompetitorDashboardBase = ({ + data, +}: CompetitorDashboardBaseProps) => { + const [activeTab] = useState("performance"); + return ( + <div className="space-y-6 p-6 bg-white"> + <div className="flex justify-between items-center"> + <div> + <h2 className="text-2xl font-bold text-slate-800"> + Competitor Analysis + </h2> + <p className="text-gray-500"> + Compare your website against top competitors + </p> + </div> + <Button variant="outline" size="sm"> + <Plus className="h-4 w-4 mr-1" /> Add Competitor + </Button> + </div> + + <MetricsOverview + yourSite={data.yourSite} + competitors={data.competitors} + /> + + <DetailedMetrics + yourSite={data.yourSite} + competitors={data.competitors} + activeTab={activeTab} + /> + </div> + ); +}; diff --git a/website-monitoring-frontend/src/components/core/competitorDashboard/DetailedMetrics.tsx b/website-monitoring-frontend/src/components/core/competitorDashboard/DetailedMetrics.tsx new file mode 100644 index 0000000..a4e05b3 --- /dev/null +++ b/website-monitoring-frontend/src/components/core/competitorDashboard/DetailedMetrics.tsx @@ -0,0 +1,372 @@ +import { useState } from "react"; +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { Badge } from "@/components/ui/layout/Badge"; +import { EnhancedChart } from "@/components/ui/data-display/EnhancedChart"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/layout/Tabs"; +import { Lightbulb } from "lucide-react"; +import { COLORS, MetricColorKey } from "@/constants/colors"; +import type { CompetitorMetrics } from "@/types/metrics"; + +interface DetailedMetricsProps { + yourSite: CompetitorMetrics; + competitors: CompetitorMetrics[]; + activeTab: string; +} + +const MetricChart = ({ + yourSite, + competitors, + metric, +}: { + yourSite: CompetitorMetrics; + competitors: CompetitorMetrics[]; + metric: MetricColorKey; +}) => { + return ( + <div className="h-64"> + <EnhancedChart + datasets={[ + { + label: "Your Website", + data: yourSite.history[metric], + color: COLORS.metrics[metric], + borderWidth: 2, + fill: false, + }, + ...competitors.map((competitor, index) => ({ + label: competitor.name, + data: competitor.history[metric], + color: COLORS.competitors[index], + borderWidth: 2, + fill: false, + })), + ]} + labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]} + options={{ + interaction: { + mode: "nearest", + intersect: false, + }, + plugins: { + legend: { + position: "top" as const, + align: "start" as const, + }, + }, + scales: { + y: { + min: 0, + max: 100, + ticks: { + stepSize: 20, + }, + }, + }, + }} + /> + </div> + ); +}; + +const PerformanceTab = ({ yourSite, competitors }: DetailedMetricsProps) => { + return ( + <div className="space-y-6"> + <div> + <h4 className="font-medium text-gray-900 mb-3">Core Web Vitals</h4> + <div className="grid grid-cols-3 gap-4"> + {Object.entries(yourSite.coreWebVitals).map(([key, value]) => ( + <div key={key} className="bg-gray-50 p-4 rounded-lg"> + <div className="text-sm font-medium text-gray-600">{key}</div> + <div className="text-2xl font-bold text-gray-900">{value}</div> + </div> + ))} + </div> + </div> + + <div> + <h4 className="font-medium text-gray-900 mb-3">Historical Trend</h4> + <MetricChart + yourSite={yourSite} + competitors={competitors} + metric="performance" + /> + </div> + + <div> + <h4 className="pt-8 font-medium text-gray-900 mb-3"> + Detailed Metrics + </h4> + <div className="grid grid-cols-2 gap-4"> + {Object.entries(yourSite.metrics.performance).map(([key, value]) => ( + <div + key={key} + className="flex items-center justify-between bg-gray-50 p-3 rounded-lg" + > + <span className="text-sm text-gray-600">{key}</span> + <span className="font-medium text-gray-900">{value}</span> + </div> + ))} + </div> + </div> + </div> + ); +}; + +const SEOTab = ({ yourSite, competitors }: DetailedMetricsProps) => { + return ( + <div className="space-y-6"> + <div> + <h4 className="font-medium text-gray-900 mb-3">SEO Metrics</h4> + <div className="grid grid-cols-2 gap-4"> + {Object.entries(yourSite.metrics.seo).map(([key, value]) => ( + <div + key={key} + className="flex items-center justify-between bg-gray-50 p-3 rounded-lg" + > + <span className="text-sm text-gray-600">{key}</span> + <span className="font-medium text-gray-900"> + {typeof value === "boolean" ? (value ? "✓" : "✗") : value} + </span> + </div> + ))} + </div> + </div> + + <div> + <h4 className="font-medium text-gray-900 mb-3">Historical Trend</h4> + <MetricChart + yourSite={yourSite} + competitors={competitors} + metric="seo" + /> + </div> + <div className="pt-4"></div> + </div> + ); +}; + +const AccessibilityTab = ({ yourSite, competitors }: DetailedMetricsProps) => { + const metrics = yourSite.metrics.accessibility || {}; + return ( + <div className="space-y-6"> + <div> + <h4 className="font-medium text-gray-900 mb-3"> + Accessibility Metrics + </h4> + <div className="grid grid-cols-2 gap-4"> + {/* Add null check before Object.entries */} + {Object.entries(metrics).map(([key, value]) => ( + <div + key={key} + className="flex items-center justify-between bg-gray-50 p-3 rounded-lg" + > + <span className="text-sm text-gray-600">{key}</span> + <span className="font-medium text-gray-900"> + {typeof value === "boolean" ? (value ? "✓" : "✗") : value} + </span> + </div> + ))} + </div> + </div> + + <div> + <h4 className="font-medium text-gray-900 mb-3">Historical Trend</h4> + <MetricChart + yourSite={yourSite} + competitors={competitors} + metric="accessibility" + /> + </div> + </div> + ); +}; + +const BestPracticesTab = ({ yourSite, competitors }: DetailedMetricsProps) => { + return ( + <div className="space-y-6"> + <div> + <h4 className="font-medium text-gray-900 mb-3"> + Best Practices Metrics + </h4> + <div className="grid grid-cols-2 gap-4"> + {/* Add relevant best practices metrics here */} + {Object.entries(yourSite.metrics.bestPractices || {}).map( + ([key, value]) => ( + <div + key={key} + className="flex items-center justify-between bg-gray-50 p-3 rounded-lg" + > + <span className="text-sm text-gray-600">{key}</span> + <span className="font-medium text-gray-900"> + {typeof value === "boolean" ? (value ? "✓" : "✗") : value} + </span> + </div> + ), + )} + </div> + </div> + + <div> + <h4 className="font-medium text-gray-900 mb-3">Historical Trend</h4> + <MetricChart + yourSite={yourSite} + competitors={competitors} + metric="bestPractices" + /> + </div> + + {/* Additional best practices specific metrics */} + <div className="pt-8"> + <h4 className="font-medium text-gray-900 mb-3">Security Checks</h4> + <div className="grid grid-cols-2 gap-4"> + <div className="bg-gray-50 p-3 rounded-lg"> + <div className="text-sm text-gray-600">HTTPS Usage</div> + <div className="font-medium text-gray-900">Enabled</div> + </div> + <div className="bg-gray-50 p-3 rounded-lg"> + <div className="text-sm text-gray-600">Mixed Content</div> + <div className="font-medium text-gray-900">None detected</div> + </div> + <div className="bg-gray-50 p-3 rounded-lg"> + <div className="text-sm text-gray-600">Security Headers</div> + <div className="font-medium text-gray-900">Properly configured</div> + </div> + <div className="bg-gray-50 p-3 rounded-lg"> + <div className="text-sm text-gray-600">Deprecated APIs</div> + <div className="font-medium text-gray-900">None found</div> + </div> + </div> + </div> + + <div> + <h4 className="font-medium text-gray-900 mb-3">Code Quality</h4> + <div className="grid grid-cols-2 gap-4"> + <div className="bg-gray-50 p-3 rounded-lg"> + <div className="text-sm text-gray-600">Console Logs</div> + <div className="font-medium text-gray-900">No errors</div> + </div> + <div className="bg-gray-50 p-3 rounded-lg"> + <div className="text-sm text-gray-600">Doctype</div> + <div className="font-medium text-gray-900">HTML5</div> + </div> + <div className="bg-gray-50 p-3 rounded-lg"> + <div className="text-sm text-gray-600">Charset</div> + <div className="font-medium text-gray-900">UTF-8</div> + </div> + <div className="bg-gray-50 p-3 rounded-lg"> + <div className="text-sm text-gray-600">JS Error Handling</div> + <div className="font-medium text-gray-900">Implemented</div> + </div> + </div> + </div> + </div> + ); +}; + +export const DetailedMetrics = ({ + yourSite, + competitors, + activeTab = "performance", +}: DetailedMetricsProps) => { + const [selectedTab, setSelectedTab] = useState(activeTab); + + const recommendations = yourSite.recommendations.filter( + (rec) => rec.category === selectedTab, + ); + + return ( + <Card className="overflow-hidden border-0 shadow-lg"> + <CardContent className="p-6"> + <Tabs + value={selectedTab} + onValueChange={setSelectedTab} + className="w-full" + > + <TabsList className="grid grid-cols-4 mb-6"> + <TabsTrigger value="performance">Performance</TabsTrigger> + <TabsTrigger value="seo">SEO</TabsTrigger> + <TabsTrigger value="accessibility">Accessibility</TabsTrigger> + <TabsTrigger value="bestPractices">Best Practices</TabsTrigger> + </TabsList> + + <TabsContent value="performance"> + <PerformanceTab + yourSite={yourSite} + competitors={competitors} + activeTab={selectedTab} + /> + </TabsContent> + + <TabsContent value="seo"> + <SEOTab + yourSite={yourSite} + competitors={competitors} + activeTab={selectedTab} + /> + </TabsContent> + + <TabsContent value="accessibility"> + <AccessibilityTab + yourSite={yourSite} + competitors={competitors} + activeTab={selectedTab} + /> + </TabsContent> + + <TabsContent value="bestPractices"> + <BestPracticesTab + yourSite={yourSite} + competitors={competitors} + activeTab={selectedTab} + /> + </TabsContent> + + {recommendations.length > 0 && ( + <div className="mt-6"> + <h4 className="font-medium text-gray-900 mb-3"> + Recommendations + </h4> + <div className="space-y-2"> + {recommendations.map((rec, index) => ( + <div + key={index} + className="flex items-start gap-2 bg-blue-50 p-3 rounded-lg" + > + <div className="text-blue-500 mt-1"> + <Lightbulb className="h-4 w-4" /> + </div> + <div> + <div className="font-medium text-blue-900"> + {rec.title} + </div> + <div className="text-sm text-blue-700"> + {rec.description} + </div> + <Badge + variant="secondary" + className={`mt-1 ${ + rec.impact === "high" + ? "bg-red-100 text-red-800" + : rec.impact === "medium" + ? "bg-yellow-100 text-yellow-800" + : "bg-blue-100 text-blue-800" + }`} + > + {rec.impact} impact + </Badge> + </div> + </div> + ))} + </div> + </div> + )} + </Tabs> + </CardContent> + </Card> + ); +}; diff --git a/website-monitoring-frontend/src/components/core/competitorDashboard/MetricsOverview.tsx b/website-monitoring-frontend/src/components/core/competitorDashboard/MetricsOverview.tsx new file mode 100644 index 0000000..5d9ba1d --- /dev/null +++ b/website-monitoring-frontend/src/components/core/competitorDashboard/MetricsOverview.tsx @@ -0,0 +1,87 @@ +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { COLORS, MetricColorKey } from "@/constants/colors"; +import { calculateOverallScore } from "@/utils/dataGenerators"; +import { CompetitorMetrics } from "@/types/metrics"; + +interface MetricsOverviewProps { + yourSite: CompetitorMetrics; + competitors: CompetitorMetrics[]; +} + +export const MetricsOverview = ({ + yourSite, + competitors, +}: MetricsOverviewProps) => { + const metrics: { key: MetricColorKey; label: string }[] = [ + { key: "performance", label: "Performance" }, + { key: "seo", label: "SEO" }, + { key: "accessibility", label: "Accessibility" }, + { key: "bestPractices", label: "Best Practices" }, + ]; + + return ( + <Card className="overflow-hidden border-0 shadow-lg mb-6"> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4"> + Overall Performance Score + </h3> + <div className="space-y-4"> + {/* Your site overview */} + <div className="flex items-center space-x-4 bg-blue-50 p-4 rounded-lg"> + <div className="flex-shrink-0 w-32"> + <div className="text-3xl font-bold text-blue-700"> + {calculateOverallScore(yourSite).toFixed(2)}% + </div> + <div className="text-sm text-blue-600 font-medium"> + Your Score + </div> + </div> + <div className="flex-grow grid grid-cols-4 gap-6"> + {metrics.map(({ key, label }) => ( + <div key={key} className="text-center"> + <div + className="text-lg font-bold" + style={{ color: COLORS.metrics[key] }} + > + {yourSite[key].toFixed(2)}% + </div> + <div className="text-sm text-gray-600">{label}</div> + </div> + ))} + </div> + </div> + + {/* Competitors overview */} + {competitors.map((competitor, index) => ( + <div + key={competitor.name} + className="flex items-center space-x-4 bg-gray-50 p-4 rounded-lg hover:bg-gray-100 transition-colors" + > + <div className="flex-shrink-0 w-32"> + <div + className="text-3xl font-bold" + style={{ color: COLORS.competitors[index] }} + > + {calculateOverallScore(competitor).toFixed(2)}% + </div> + <div className="text-sm text-gray-600 font-medium"> + {competitor.name} + </div> + </div> + <div className="flex-grow grid grid-cols-4 gap-6"> + {metrics.map(({ key, label }) => ( + <div key={key} className="text-center"> + <div className="text-lg font-bold text-gray-700"> + {competitor[key].toFixed(2)}% + </div> + <div className="text-sm text-gray-500">{label}</div> + </div> + ))} + </div> + </div> + ))} + </div> + </CardContent> + </Card> + ); +}; diff --git a/website-monitoring-frontend/src/components/core/dashboard/DashboardBase.tsx b/website-monitoring-frontend/src/components/core/dashboard/DashboardBase.tsx new file mode 100644 index 0000000..00806c1 --- /dev/null +++ b/website-monitoring-frontend/src/components/core/dashboard/DashboardBase.tsx @@ -0,0 +1,38 @@ +import { DashboardLayout } from "@/components/common/DashboardLayout"; +import { MetricsGrid } from "./MetricsGrid"; +import { PerformanceSection } from "./PerformanceSection"; +import { ResourceSection } from "./ResourceSection"; +import { StatusSection } from "./StatusSection"; +import type { DashboardMetrics } from "@/types/metrics"; + +interface DashboardBaseProps { + data: DashboardMetrics; + isDemo?: boolean; +} + +export const DashboardBase = ({ data, isDemo = false }: DashboardBaseProps) => { + return ( + <DashboardLayout> + <div className="space-y-4 px-4 py-4 bg-white"> + <MetricsGrid + performance={data.performance ?? 0} + seo={data.seo ?? 0} + accessibility={data.accessibility ?? 0} + bestPractices={data.bestPractices ?? 0} + animated={isDemo} + /> + <PerformanceSection + performance={data.history?.performance} + uptime={data.history?.uptime} + labels={data.history?.dates} + /> + <ResourceSection resources={data.resources} /> + <StatusSection + criticalIssues={data.criticalIssues ?? []} + deviceMetrics={data.devices} + timeline={data.timeline ?? []} + /> + </div> + </DashboardLayout> + ); +}; diff --git a/website-monitoring-frontend/src/components/core/dashboard/MetricsGrid.tsx b/website-monitoring-frontend/src/components/core/dashboard/MetricsGrid.tsx new file mode 100644 index 0000000..aacbc80 --- /dev/null +++ b/website-monitoring-frontend/src/components/core/dashboard/MetricsGrid.tsx @@ -0,0 +1,43 @@ +import { MetricsCard } from "@/components/dashboard/MetricsCard"; + +interface MetricsGridProps { + performance: number; + seo: number; + accessibility: number; + bestPractices: number; + animated?: boolean; +} + +export const MetricsGrid = ({ + performance, + seo, + accessibility, + bestPractices, + animated = false, +}: MetricsGridProps) => { + return ( + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> + <MetricsCard + title="Performance" + value={performance} + previousValue={performance - (animated ? Math.random() * 5 : 0)} + isPercentage={true} + /> + <MetricsCard + title="SEO" + value={seo} + previousValue={seo - (animated ? Math.random() * 0.05 : 0)} + /> + <MetricsCard + title="Accessibility" + value={accessibility} + previousValue={accessibility - (animated ? Math.random() * 0.05 : 0)} + /> + <MetricsCard + title="Best Practices" + value={bestPractices} + previousValue={bestPractices - (animated ? Math.random() * 0.05 : 0)} + /> + </div> + ); +}; diff --git a/website-monitoring-frontend/src/components/core/dashboard/PerformanceSection.tsx b/website-monitoring-frontend/src/components/core/dashboard/PerformanceSection.tsx new file mode 100644 index 0000000..1b2745f --- /dev/null +++ b/website-monitoring-frontend/src/components/core/dashboard/PerformanceSection.tsx @@ -0,0 +1,31 @@ +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { PerformanceChart } from "@/components/dashboard/PerformanceChart"; + +interface PerformanceSectionProps { + performance?: number[]; + uptime?: number[]; + labels?: string[]; +} + +export const PerformanceSection = ({ + performance = [], + uptime = [], + labels = [], +}: PerformanceSectionProps) => { + return ( + <Card className="overflow-hidden border-0 shadow-lg"> + <CardContent className="p-4"> + <h3 className="text-lg font-semibold mb-4 text-slate-800"> + Performance History + </h3> + <div className="h-64"> + <PerformanceChart + performance={performance} + uptime={uptime} + labels={labels} + /> + </div> + </CardContent> + </Card> + ); +}; diff --git a/website-monitoring-frontend/src/components/core/dashboard/ResourceSection.tsx b/website-monitoring-frontend/src/components/core/dashboard/ResourceSection.tsx new file mode 100644 index 0000000..aa4dd73 --- /dev/null +++ b/website-monitoring-frontend/src/components/core/dashboard/ResourceSection.tsx @@ -0,0 +1,64 @@ +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { Code, FileText, Image as ImgIcon, File, Archive, Film, Type } from "lucide-react"; +import type { ResourceMetrics } from "@/types/metrics"; + +const getResourceIcon = (key: string) => { + switch (key) { + case "script": + return <Code className="h-5 w-5 text-blue-500" />; + case "stylesheet": + return <FileText className="h-5 w-5 text-green-500" />; + case "image": + return <ImgIcon className="h-5 w-5 text-purple-500" />; + case "font": + return <Type className="h-5 w-5 text-orange-500" />; + case "document": + return <File className="h-5 w-5 text-indigo-500" />; + case "media": + return <Film className="h-5 w-5 text-red-500" />; + case "other": + return <Archive className="h-5 w-5 text-gray-500" />; + default: + return <File className="h-5 w-5 text-gray-500" />; + } +}; + +export const ResourceSection = ({ resources }: { resources: ResourceMetrics }) => { + const total = resources.total; + return ( + <Card className="overflow-hidden border-0 shadow-lg"> + <CardContent className="p-4"> + <h3 className="text-lg font-semibold mb-4 text-slate-800"> + Resource Breakdown + </h3> + <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> + {Object.entries(resources).map(([key, value]) => { + if (key === "total") return null; + // Prozentualer Anteil, leicht variierend, um "glatte" Werte zu vermeiden: + const basePercent = (value / total) * 100; + const percent = (basePercent + Math.random() * 2 - 1).toFixed(1); + return ( + <div + key={key} + className="bg-gray-50 p-3 rounded-lg flex flex-col items-center space-y-2" + > + <div className="flex items-center space-x-1"> + {getResourceIcon(key)} + <span className="text-sm text-gray-600 capitalize"> + {key} + </span> + </div> + <div className="text-xl font-bold text-slate-800"> + {value} KB + </div> + <div className="text-xs text-gray-500"> + {percent}% of total + </div> + </div> + ); + })} + </div> + </CardContent> + </Card> + ); +}; diff --git a/website-monitoring-frontend/src/components/core/dashboard/StatusSection.tsx b/website-monitoring-frontend/src/components/core/dashboard/StatusSection.tsx new file mode 100644 index 0000000..cc575f3 --- /dev/null +++ b/website-monitoring-frontend/src/components/core/dashboard/StatusSection.tsx @@ -0,0 +1,49 @@ +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { DeviceComparison } from "@/components/dashboard/DeviceComparison"; +import { TimelineView } from "@/components/dashboard/TimelineView"; +import { CriticalIssuesList } from "@/components/dashboard/CriticalIssuesList"; +import type { + CriticalIssue, + DeviceMetrics, + TimelineEvent, +} from "@/types/metrics"; + +interface StatusSectionProps { + criticalIssues: CriticalIssue[]; + deviceMetrics: { + desktop: DeviceMetrics; + mobile: DeviceMetrics; + }; + timeline: TimelineEvent[]; +} + +export const StatusSection = ({ + criticalIssues, + deviceMetrics, + timeline, +}: StatusSectionProps) => { + return ( + <div className="space-y-6"> + <Card className="overflow-hidden border-0 shadow-lg"> + <CardContent className="p-4"> + <CriticalIssuesList issues={criticalIssues} /> + </CardContent> + </Card> + + <Card className="overflow-hidden border-0 shadow-lg"> + <CardContent className="p-4"> + <DeviceComparison + desktop={deviceMetrics.desktop} + mobile={deviceMetrics.mobile} + /> + </CardContent> + </Card> + + <Card className="overflow-hidden border-0 shadow-lg"> + <CardContent className="p-4"> + <TimelineView events={timeline} /> + </CardContent> + </Card> + </div> + ); +}; diff --git a/website-monitoring-frontend/src/components/dashboard/AddWebsiteForm.tsx b/website-monitoring-frontend/src/components/dashboard/AddWebsiteForm.tsx new file mode 100644 index 0000000..0fc2bd1 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/AddWebsiteForm.tsx @@ -0,0 +1,666 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { motion, AnimatePresence } from "framer-motion"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, + FormDescription, +} from "@/components/ui/forms/Form"; +import { Input } from "@/components/ui/forms/Input"; +import { Button } from "@/components/ui/forms/Button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/layout/Card"; +import { Badge } from "@/components/ui/layout/Badge"; +import { supabase } from "@/lib/supabase"; +import { useAuth } from "@/contexts/AuthContext"; +import { + Globe, + Check, + AlertCircle, + Loader2, + ArrowRight, + Shield, + Zap, + Search, + Eye, + ExternalLink, + ChevronRight, + Info, +} from "lucide-react"; +import { monitoringService } from "@/services/monitoringService"; + +const urlRegex = + /^https?:\/\/(?:[-\w.])+(?:\:[0-9]+)?(?:\/(?:[\w\/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?$/; + +const formSchema = z.object({ + name: z + .string() + .min(2, "Website name must be at least 2 characters") + .max(50, "Website name must be less than 50 characters"), + url: z + .string() + .url("Please enter a valid URL") + .refine( + (url) => urlRegex.test(url), + "Please enter a valid website URL (must include http:// or https://)", + ), +}); + +interface ValidationResult { + isValid: boolean; + favicon?: string; + title?: string; + description?: string; + error?: string; +} + +const steps = [ + { + id: 1, + title: "Website Details", + description: "Enter your website information", + }, + { + id: 2, + title: "Configuration", + description: "Set up monitoring preferences", + }, + { id: 3, title: "Complete", description: "Start monitoring your website" }, +]; + +export function AddWebsiteForm() { + const [currentStep, setCurrentStep] = useState(1); + const [loading, setLoading] = useState(false); + const [validating, setValidating] = useState(false); + const [validation, setValidation] = useState<ValidationResult | null>(null); + const [error, setError] = useState(""); + + const router = useRouter(); + const { userDetails } = useAuth(); + + const form = useForm<z.infer<typeof formSchema>>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + url: "", + }, + }); + + const watchedUrl = form.watch("url"); + + useEffect(() => { + if (userDetails && !userDetails.organization_id) { + router.push("/dashboard/organizations/new"); + } + }, [userDetails, router]); + + // Validate URL when it changes + useEffect(() => { + const validateUrl = async () => { + if (!watchedUrl || !urlRegex.test(watchedUrl)) { + setValidation(null); + return; + } + + setValidating(true); + try { + // Try to fetch basic info about the website + const response = await fetch(`/api/validate-website`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: watchedUrl }), + }); + + if (response.ok) { + const data = await response.json(); + setValidation({ + isValid: true, + favicon: data.favicon, + title: data.title, + description: data.description, + }); + + // Auto-fill name if empty + if (!form.getValues("name") && data.title) { + form.setValue("name", data.title); + } + } else { + setValidation({ + isValid: false, + error: + "Unable to validate website. It might be unreachable or have access restrictions.", + }); + } + } catch { + setValidation({ + isValid: false, + error: + "Failed to validate website. Please check the URL and try again.", + }); + } finally { + setValidating(false); + } + }; + + const debounceTimer = setTimeout(validateUrl, 1000); + return () => clearTimeout(debounceTimer); + }, [watchedUrl, form]); + + const onSubmit = async (values: z.infer<typeof formSchema>) => { + if (currentStep === 1) { + if (validation?.isValid !== true) { + setError( + "Please ensure the website URL is valid and accessible before proceeding.", + ); + return; + } + setCurrentStep(2); + return; + } + + if (currentStep === 2) { + try { + setLoading(true); + setError(""); + + if (!userDetails?.organization_id) { + throw new Error( + "Organization not found. Please refresh and try again.", + ); + } + + // Normalize URL + let baseUrl = values.url; + if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) { + baseUrl = `https://${baseUrl}`; + } + baseUrl = baseUrl.replace(/\/+$/, ""); + + const { data, error: insertError } = await supabase + .from("websites") + .insert([ + { + name: values.name, + base_url: baseUrl, + organization_id: userDetails.organization_id, + is_active: true, + crawl_settings: { + max_depth: 3, + max_pages: 100, + exclude_patterns: [ + "/admin/*", + "/api/*", + "*.pdf", + "*.jpg", + "*.png", + ], + include_patterns: ["/*"], + respect_robots_txt: true, + crawl_frequency: "daily", + }, + }, + ]) + .select(); + + if (insertError) { + throw new Error(`Failed to add website: ${insertError.message}`); + } + + setCurrentStep(3); + + // Trigger initial scan/crawl in the background (do not await) + if (data && data.length > 0) { + monitoringService.initiateAnalysis(data[0].id).catch(() => {}); + setTimeout(() => { + router.push(`/dashboard/websites/${data[0].id}`); + }, 2000); + } + } catch (error) { + console.error("Failed to add website:", error); + setError( + error instanceof Error ? error.message : "An unknown error occurred", + ); + } finally { + setLoading(false); + } + } + }; + + const handleBack = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1); + } else { + router.back(); + } + }; + + return ( + <div className="max-w-4xl mx-auto space-y-8"> + {/* Header */} + <div className="text-center space-y-4"> + <motion.div + initial={{ opacity: 0, y: -20 }} + animate={{ opacity: 1, y: 0 }} + className="flex items-center justify-center gap-3" + > + <div className="p-3 bg-blue-100 rounded-full"> + <Globe className="w-8 h-8 text-blue-600" /> + </div> + </motion.div> + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.1 }} + > + <h1 className="text-3xl font-bold text-gray-900">Add New Website</h1> + <p className="text-gray-600 mt-2"> + Start monitoring your website's performance, SEO, and + accessibility + </p> + </motion.div> + </div> + + {/* Progress Steps */} + <div className="flex items-center justify-center"> + <div className="flex items-center space-x-8"> + {steps.map((step, index) => ( + <motion.div + key={step.id} + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ delay: index * 0.1 }} + className="flex items-center" + > + <div className="flex items-center"> + <div + className={`w-10 h-10 rounded-full flex items-center justify-center border-2 transition-all ${ + currentStep >= step.id + ? "bg-blue-600 border-blue-600 text-white" + : "bg-white border-gray-300 text-gray-400" + }`} + > + {currentStep > step.id ? ( + <Check className="w-5 h-5" /> + ) : ( + <span className="text-sm font-medium">{step.id}</span> + )} + </div> + <div className="ml-3 hidden sm:block"> + <p + className={`text-sm font-medium ${currentStep >= step.id ? "text-blue-600" : "text-gray-400"}`} + > + {step.title} + </p> + <p className="text-xs text-gray-500">{step.description}</p> + </div> + </div> + {index < steps.length - 1 && ( + <ChevronRight className="w-5 h-5 text-gray-300 mx-4" /> + )} + </motion.div> + ))} + </div> + </div> + + {/* Form Content */} + <Card className="border-0 shadow-lg"> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <AnimatePresence mode="wait"> + {/* Step 1: Website Details */} + {currentStep === 1 && ( + <motion.div + key="step1" + initial={{ opacity: 0, x: 20 }} + animate={{ opacity: 1, x: 0 }} + exit={{ opacity: 0, x: -20 }} + > + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Globe className="w-5 h-5 text-blue-600" /> + Website Information + </CardTitle> + </CardHeader> + <CardContent className="space-y-6"> + <FormField + control={form.control} + name="url" + render={({ field }) => ( + <FormItem> + <FormLabel>Website URL</FormLabel> + <FormControl> + <div className="relative"> + <Input + {...field} + placeholder="https://example.com" + className="pr-10" + /> + <div className="absolute right-3 top-1/2 -translate-y-1/2"> + {validating ? ( + <Loader2 className="w-4 h-4 animate-spin text-gray-400" /> + ) : validation?.isValid ? ( + <Check className="w-4 h-4 text-green-500" /> + ) : validation?.error ? ( + <AlertCircle className="w-4 h-4 text-red-500" /> + ) : null} + </div> + </div> + </FormControl> + <FormDescription> + Enter the full URL of your website including + https:// + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* URL Validation Result */} + {validation && ( + <motion.div + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + className={`p-4 rounded-lg border ${ + validation.isValid + ? "bg-green-50 border-green-200" + : "bg-red-50 border-red-200" + }`} + > + {validation.isValid ? ( + <div className="flex items-start gap-3"> + {validation.favicon && ( + <img + src={validation.favicon} + alt="Website favicon" + className="w-6 h-6 rounded" + /> + )} + <div className="flex-1"> + <p className="font-medium text-green-800"> + Website validated successfully! + </p> + {validation.title && ( + <p className="text-sm text-blue-700 mt-1"> + Found: {validation.title} + </p> + )} + {validation.description && ( + <p className="text-xs text-green-600 mt-1 line-clamp-2"> + {validation.description} + </p> + )} + </div> + <ExternalLink className="w-4 h-4 text-green-600" /> + </div> + ) : ( + <div className="flex items-start gap-3"> + <AlertCircle className="w-5 h-5 text-red-500 mt-0.5" /> + <div> + <p className="font-medium text-red-800"> + Unable to validate website + </p> + <p className="text-sm text-red-700 mt-1"> + {validation.error} + </p> + </div> + </div> + )} + </motion.div> + )} + + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>Website Name</FormLabel> + <FormControl> + <Input + {...field} + placeholder="My Awesome Website" + /> + </FormControl> + <FormDescription> + A recognizable name for your website in the + dashboard + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {error && ( + <div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3"> + <AlertCircle className="w-5 h-5 text-red-500" /> + <span className="text-red-800">{error}</span> + </div> + )} + </CardContent> + </motion.div> + )} + + {/* Step 2: Configuration */} + {currentStep === 2 && ( + <motion.div + key="step2" + initial={{ opacity: 0, x: 20 }} + animate={{ opacity: 1, x: 0 }} + exit={{ opacity: 0, x: -20 }} + > + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Zap className="w-5 h-5 text-blue-600" /> + Monitoring Configuration + </CardTitle> + </CardHeader> + <CardContent className="space-y-6"> + <div className="bg-blue-50 border border-blue-200 rounded-lg p-6"> + <div className="flex items-start gap-4"> + <Info className="w-5 h-5 text-blue-600 mt-0.5" /> + <div> + <h3 className="font-medium text-blue-900 mb-2"> + What we'll monitor for you + </h3> + <div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> + <div className="flex items-center gap-2"> + <Zap className="w-4 h-4 text-blue-600" /> + <span className="text-sm text-blue-800"> + Performance metrics + </span> + </div> + <div className="flex items-center gap-2"> + <Search className="w-4 h-4 text-blue-600" /> + <span className="text-sm text-blue-800"> + SEO optimization + </span> + </div> + <div className="flex items-center gap-2"> + <Eye className="w-4 h-4 text-blue-600" /> + <span className="text-sm text-blue-800"> + Accessibility compliance + </span> + </div> + <div className="flex items-center gap-2"> + <Shield className="w-4 h-4 text-blue-600" /> + <span className="text-sm text-blue-800"> + Security best practices + </span> + </div> + </div> + </div> + </div> + </div> + + <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> + <div className="bg-gray-50 rounded-lg p-4"> + <h4 className="font-medium text-gray-900 mb-2"> + Crawl Depth + </h4> + <Badge variant="blue">3 levels</Badge> + <p className="text-sm text-gray-600 mt-2"> + We'll discover and analyze up to 3 levels deep + from your homepage + </p> + </div> + <div className="bg-gray-50 rounded-lg p-4"> + <h4 className="font-medium text-gray-900 mb-2"> + Page Limit + </h4> + <Badge variant="green">100 pages</Badge> + <p className="text-sm text-gray-600 mt-2"> + Maximum number of pages to analyze per scan + </p> + </div> + <div className="bg-gray-50 rounded-lg p-4"> + <h4 className="font-medium text-gray-900 mb-2"> + Scan Frequency + </h4> + <Badge variant="purple">Daily</Badge> + <p className="text-sm text-gray-600 mt-2"> + Automatic scans will run once per day + </p> + </div> + </div> + + <div className="bg-gray-50 rounded-lg p-4"> + <h4 className="font-medium text-gray-900 mb-2"> + Default Exclusions + </h4> + <div className="flex flex-wrap gap-2"> + {["/admin/*", "/api/*", "*.pdf", "*.jpg", "*.png"].map( + (pattern) => ( + <Badge + key={pattern} + variant="gray" + className="text-xs" + > + {pattern} + </Badge> + ), + )} + </div> + <p className="text-sm text-gray-600 mt-2"> + These patterns will be excluded from scanning. You can + modify these later in settings. + </p> + </div> + + {error && ( + <div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3"> + <AlertCircle className="w-5 h-5 text-red-500" /> + <span className="text-red-800">{error}</span> + </div> + )} + </CardContent> + </motion.div> + )} + + {/* Step 3: Success */} + {currentStep === 3 && ( + <motion.div + key="step3" + initial={{ opacity: 0, scale: 0.95 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0.95 }} + > + <CardContent className="text-center py-12"> + <motion.div + initial={{ scale: 0 }} + animate={{ scale: 1 }} + transition={{ + delay: 0.2, + type: "spring", + stiffness: 200, + }} + className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6" + > + <Check className="w-8 h-8 text-green-600" /> + </motion.div> + <motion.h2 + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.3 }} + className="text-2xl font-bold text-gray-900 mb-3" + > + Website Added Successfully! + </motion.h2> + <motion.p + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.4 }} + className="text-gray-600 mb-8" + > + Your website is now being monitored. We'll start the + initial scan and you'll be redirected to the website + dashboard shortly. + </motion.p> + <motion.div + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.5 }} + className="flex items-center justify-center gap-2" + > + <Loader2 className="w-4 h-4 animate-spin text-blue-600" /> + <span className="text-sm text-blue-600"> + Redirecting to dashboard... + </span> + </motion.div> + </CardContent> + </motion.div> + )} + </AnimatePresence> + + {/* Action Buttons */} + {currentStep < 3 && ( + <div className="border-t border-gray-200 px-6 py-4 flex justify-between"> + <Button + type="button" + variant="outline" + onClick={handleBack} + disabled={loading} + > + {currentStep === 1 ? "Cancel" : "Back"} + </Button> + <Button + type="submit" + disabled={ + loading || + (currentStep === 1 && validation?.isValid !== true) + } + className="flex items-center gap-2" + > + {loading ? ( + <Loader2 className="w-4 h-4 animate-spin" /> + ) : currentStep === 1 ? ( + <> + Next Step + <ArrowRight className="w-4 h-4" /> + </> + ) : ( + <> + Add Website + <Check className="w-4 h-4" /> + </> + )} + </Button> + </div> + )} + </form> + </Form> + </Card> + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/dashboard/CrawlerControl.tsx b/website-monitoring-frontend/src/components/dashboard/CrawlerControl.tsx new file mode 100644 index 0000000..71a1ac2 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/CrawlerControl.tsx @@ -0,0 +1,901 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Button } from "@/components/ui/forms/Button"; +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { Badge } from "@/components/ui/layout/Badge"; +import { Progress } from "@/components/ui/forms/Progress"; +import { + Play, + Square, + RefreshCw, + Globe, + CheckCircle, + XCircle, + Clock, + Activity, + Search, + Eye, + EyeOff, + AlertCircle, + BarChart3, + Settings, + ExternalLink, + Download, + Filter, +} from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import { supabase } from "@/lib/supabase"; +import { scanService } from "@/services/scanService"; +import { useCrawlProgress } from "@/services/crawlManager"; + +interface CrawlerControlProps { + websiteId: string; +} + +interface Page { + id: string; + url: string; + title: string; + is_active: boolean; + depth: number; + created_at: string; + status_code?: number; + last_scan?: { + id: string; + status: string; + performance_score?: number; + created_at: string; + }; +} + +interface CrawlSession { + id: string; + status: string; + pages_discovered: number; + pages_processed: number; + started_at: string; + completed_at?: string; + error_message?: string; +} + +export function CrawlerControl({ websiteId }: CrawlerControlProps) { + const [isLoading, setIsLoading] = useState(false); + const [isLoadingPages, setIsLoadingPages] = useState(false); + const [crawlSession, setCrawlSession] = useState<CrawlSession | null>(null); + const [pages, setPages] = useState<Page[]>([]); + const [selectedPages, setSelectedPages] = useState<string[]>([]); + const [filterStatus, setFilterStatus] = useState<string>("all"); + const [isScanning, setIsScanning] = useState(false); + const [scanProgress, setScanProgress] = useState<{ [key: string]: string }>( + {}, + ); + + // Load initial data first + useEffect(() => { + loadPages(); + loadActiveCrawlSession(); + }, [websiteId]); + + // Use new crawl progress tracking (after crawlSession is loaded) + const { progress: crawlProgress, isLoading: crawlProgressLoading } = useCrawlProgress( + websiteId, + crawlSession?.id + ); + + // Set up real-time subscription for pages and crawl sessions + useEffect(() => { + if (!websiteId) return; + + const subscription = supabase + .channel(`crawler_${websiteId}`) + .on( + "postgres_changes", + { + event: "INSERT", + schema: "public", + table: "pages", + filter: `website_id=eq.${websiteId}`, + }, + (payload) => { + console.log("New page detected:", payload.new); + // Reload pages when a new page is inserted + loadPages(); + } + ) + .on( + "postgres_changes", + { + event: "UPDATE", + schema: "public", + table: "pages", + filter: `website_id=eq.${websiteId}`, + }, + (payload) => { + console.log("Page updated:", payload.new); + // Reload pages when a page is updated + loadPages(); + } + ) + .on( + "postgres_changes", + { + event: "UPDATE", + schema: "public", + table: "crawl_sessions", + filter: `website_id=eq.${websiteId}`, + }, + (payload) => { + console.log("Crawl session updated:", payload.new); + // Reload crawl session when it's updated + loadActiveCrawlSession(); + } + ) + .subscribe(); + + return () => { + subscription.unsubscribe(); + }; + }, [websiteId]); + + // Load active crawl session when component mounts + useEffect(() => { + loadActiveCrawlSession(); + }, [crawlProgress.status]); // Reload when crawl status changes + + // Reload pages when crawl completes + useEffect(() => { + if (crawlProgress.status === "completed" || crawlProgress.status === "failed") { + console.log("Crawl completed, reloading pages..."); + // Add a small delay to ensure the database has been updated + setTimeout(() => { + loadPages(); + loadActiveCrawlSession(); + }, 1000); + } + }, [crawlProgress.status]); + + // Also reload pages when the component mounts or websiteId changes + useEffect(() => { + const interval = setInterval(() => { + if (crawlSession?.status === "running") { + loadActiveCrawlSession(); + } + }, 2000); // Check every 2 seconds during active crawl + + return () => clearInterval(interval); + }, [crawlSession?.status]); + + const loadPages = async () => { + try { + setIsLoadingPages(true); + const { data, error } = await supabase + .from("pages") + .select( + ` + id, + url, + title, + is_active, + depth, + created_at, + status_code, + scans ( + id, + status, + created_at, + scan_results ( + category, + score + ) + ) + `, + ) + .eq("website_id", websiteId) + .order("depth", { ascending: true }) + .order("created_at", { ascending: false }); + + if (error) throw error; + + const pagesWithScans = + data?.map((page) => ({ + ...page, + last_scan: page.scans?.[0] + ? { + id: page.scans[0].id, + status: page.scans[0].status, + performance_score: (page.scans[0].scan_results || []).find((r: any) => r.category === "performance")?.score, + created_at: page.scans[0].created_at, + } + : undefined, + })) || []; + + setPages(pagesWithScans); + setSelectedPages( + pagesWithScans.filter((p) => p.is_active).map((p) => p.id), + ); + } catch (error) { + console.error("Failed to load pages:", error); + } finally { + setIsLoadingPages(false); + } + }; + + const loadActiveCrawlSession = async () => { + try { + const { data, error } = await supabase + .from("crawl_sessions") + .select("*") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }) + .limit(1) + .single(); + + if (error && error.code !== "PGRST116") throw error; + setCrawlSession(data); + } catch (error) { + console.error("Failed to load crawl session:", error); + } + }; + + const startCrawl = async () => { + try { + setIsLoading(true); + + if (!websiteId) { + throw new Error("No websiteId provided"); + } + + const response = await fetch("/api/crawl", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ websiteId }), + }); + + let result; + try { + result = await response.json(); + } catch (jsonError) { + console.error("Failed to parse JSON response:", jsonError); + const textResponse = await response.text(); + console.log("Raw response text:", textResponse); + throw new Error(`Invalid JSON response: ${textResponse}`); + } + + if (!response.ok) { + console.error("Crawl API error response:", { + status: response.status, + statusText: response.statusText, + result: result + }); + throw new Error(result.error || `HTTP ${response.status}: ${response.statusText}`); + } + + // Use the returned sessionId to immediately update the crawl session + if (result.sessionId) { + // Set the crawl session immediately so real-time updates work + setCrawlSession({ + id: result.sessionId, + status: "running", + pages_discovered: 0, + pages_processed: 0, + started_at: new Date().toISOString(), + }); + } + + await loadActiveCrawlSession(); + await loadPages(); + } catch (error) { + console.error("Failed to start crawl:", error); + alert( + "Failed to start crawl: " + + (error instanceof Error ? error.message : "Unknown error"), + ); + } finally { + setIsLoading(false); + } + }; + + const stopCrawl = async () => { + if (!crawlSession) return; + + try { + setIsLoading(true); + // Update session status to cancelled using new fields + await supabase + .from("crawl_sessions") + .update({ + status: "cancelled", + progress_percentage: crawlProgress.progressPercentage || 0, + completed_at: new Date().toISOString(), + }) + .eq("id", crawlSession.id); + + await loadActiveCrawlSession(); + } catch (error) { + console.error("Failed to stop crawl:", error); + } finally { + setIsLoading(false); + } + }; + + const togglePageSelection = (pageId: string) => { + setSelectedPages((prev) => + prev.includes(pageId) + ? prev.filter((id) => id !== pageId) + : [...prev, pageId], + ); + }; + + const updatePageStatus = async (pageId: string, isActive: boolean) => { + try { + const { error } = await supabase + .from("pages") + .update({ is_active: isActive }) + .eq("id", pageId); + + if (error) throw error; + await loadPages(); + } catch (error) { + console.error("Failed to update page status:", error); + } + }; + + const savePageSelection = async () => { + try { + setIsLoading(true); + + // Update all pages based on selection + const updates = pages.map((page) => ({ + id: page.id, + is_active: selectedPages.includes(page.id), + })); + + for (const update of updates) { + await supabase + .from("pages") + .update({ is_active: update.is_active }) + .eq("id", update.id); + } + + await loadPages(); + } catch (error) { + console.error("Failed to save page selection:", error); + } finally { + setIsLoading(false); + } + }; + + const startScanning = async () => { + const activePageIds = pages.filter((p) => p.is_active).map((p) => p.id); + if (activePageIds.length === 0) { + alert("Please activate at least one page to scan"); + return; + } + + try { + setIsScanning(true); + setScanProgress({}); + + // Start scans for selected pages + const scanPromises = activePageIds.map(async (pageId) => { + setScanProgress((prev) => ({ ...prev, [pageId]: "starting" })); + + const result = await scanService.scanPage(pageId); + if (result.success) { + setScanProgress((prev) => ({ ...prev, [pageId]: "running" })); + + // Monitor scan progress + const monitorScan = async (scanId: string) => { + const checkStatus = async () => { + try { + const { data } = await supabase + .from("scans") + .select("status") + .eq("id", scanId) + .single(); + + if (data?.status === "completed") { + setScanProgress((prev) => ({ + ...prev, + [pageId]: "completed", + })); + await loadPages(); + } else if (data?.status === "failed") { + setScanProgress((prev) => ({ ...prev, [pageId]: "failed" })); + } else if (data?.status === "running") { + setTimeout(checkStatus, 2000); + } + } catch (error) { + setScanProgress((prev) => ({ ...prev, [pageId]: "failed" })); + } + }; + checkStatus(); + }; + + if (result.scanId) { + monitorScan(result.scanId); + } + } else { + setScanProgress((prev) => ({ ...prev, [pageId]: "failed" })); + } + }); + + await Promise.all(scanPromises); + } catch (error) { + console.error("Failed to start scanning:", error); + } finally { + setIsScanning(false); + } + }; + + const getPageStatusIcon = (page: Page) => { + const scanStatus = scanProgress[page.id]; + if (scanStatus === "running") { + return <Activity className="w-4 h-4 text-blue-600 animate-pulse" />; + } else if (scanStatus === "completed") { + return <CheckCircle className="w-4 h-4 text-green-600" />; + } else if (scanStatus === "failed") { + return <XCircle className="w-4 h-4 text-red-600" />; + } else if (page.last_scan) { + switch (page.last_scan.status) { + case "completed": + return <CheckCircle className="w-4 h-4 text-green-600" />; + case "running": + return <Activity className="w-4 h-4 text-blue-600 animate-pulse" />; + case "failed": + return <XCircle className="w-4 h-4 text-red-600" />; + default: + return <Clock className="w-4 h-4 text-gray-600" />; + } + } + return <Globe className="w-4 h-4 text-gray-400" />; + }; + + const getScoreColor = (score: number) => { + if (score >= 90) return "text-green-600"; + if (score >= 70) return "text-yellow-600"; + return "text-red-600"; + }; + + const getCrawlProgress = () => { + // Use new crawl progress system + if (crawlProgress.progressPercentage > 0) { + return crawlProgress.progressPercentage; + } + // Fallback to session data if new system not available + if (!crawlSession) return 0; + if (crawlSession.pages_discovered === 0) return 0; + return Math.round( + (crawlSession.pages_processed / crawlSession.pages_discovered) * 100, + ); + }; + + const getCurrentCrawlStatus = () => { + return crawlProgress.status !== "pending" ? crawlProgress.status : crawlSession?.status || "Not started"; + }; + + // Dedupliziere Seiten nach URL, bevor sie angezeigt werden, und bevorzuge aktive Seiten + const dedupedPagesMap = new Map<string, Page>(); + pages.forEach((page) => { + const existing = dedupedPagesMap.get(page.url); + if (!existing) { + dedupedPagesMap.set(page.url, page); + } else if (!existing.is_active && page.is_active) { + // Bevorzuge aktive Seite, falls vorhanden + dedupedPagesMap.set(page.url, page); + } + }); + const dedupedPages = Array.from(dedupedPagesMap.values()); + + // Sortiere nach depth (aufsteigend), dann nach URL + dedupedPages.sort((a, b) => { + if (a.depth !== b.depth) return a.depth - b.depth; + return a.url.localeCompare(b.url); + }); + + // Filter anwenden + const filteredPages = dedupedPages.filter((page) => { + if (filterStatus === "active" && !page.is_active) return false; + if (filterStatus === "inactive" && page.is_active) return false; + return true; + }); + + const selectAll = async () => { + try { + setIsLoading(true); + + // Optimistically update the UI state immediately + const updatedPages = pages.map(page => ({ + ...page, + is_active: filteredPages.some(fp => fp.id === page.id) ? true : page.is_active + })); + setPages(updatedPages); + setSelectedPages(filteredPages.map((p) => p.id)); + + // Update all filtered pages to active in the background + const updates = filteredPages.map((page) => ({ + id: page.id, + is_active: true, + })); + + // Use Promise.all for better performance + await Promise.all( + updates.map(update => + supabase + .from("pages") + .update({ is_active: update.is_active }) + .eq("id", update.id) + ) + ); + + // Reload pages to ensure consistency (but don't block the UI) + loadPages().catch(error => { + console.error("Failed to reload pages after select:", error); + }); + } catch (error) { + console.error("Failed to select all pages:", error); + // Revert optimistic update on error + await loadPages(); + } finally { + setIsLoading(false); + } + }; + + const deselectAll = async () => { + try { + setIsLoading(true); + + // Optimistically update the UI state immediately + const updatedPages = pages.map(page => ({ + ...page, + is_active: filteredPages.some(fp => fp.id === page.id) ? false : page.is_active + })); + setPages(updatedPages); + setSelectedPages([]); + + // Update all filtered pages to inactive in the background + const updates = filteredPages.map((page) => ({ + id: page.id, + is_active: false, + })); + + // Use Promise.all for better performance + await Promise.all( + updates.map(update => + supabase + .from("pages") + .update({ is_active: update.is_active }) + .eq("id", update.id) + ) + ); + + // Reload pages to ensure consistency (but don't block the UI) + loadPages().catch(error => { + console.error("Failed to reload pages after deselect:", error); + }); + } catch (error) { + console.error("Failed to deselect all pages:", error); + // Revert optimistic update on error + await loadPages(); + } finally { + setIsLoading(false); + } + }; + + return ( + <div className="space-y-6"> + {/* Crawl Status */} + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + className="grid grid-cols-1 md:grid-cols-2 gap-6" + > + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between mb-4"> + <h3 className="text-lg font-semibold">Crawl Status</h3> + <Badge + className={ + getCurrentCrawlStatus() === "running" + ? "bg-blue-100 text-blue-800" + : getCurrentCrawlStatus() === "completed" + ? "bg-green-100 text-green-800" + : getCurrentCrawlStatus() === "failed" + ? "bg-red-100 text-red-800" + : "bg-gray-100 text-gray-800" + } + > + {getCurrentCrawlStatus()} + </Badge> + </div> + + {(crawlSession || crawlProgress.totalUrls > 0) && ( + <div className="space-y-3"> + <div className="flex justify-between text-sm"> + <span>URLs Discovered:</span> + <span className="font-medium"> + {crawlProgress.totalUrls || crawlSession?.pages_discovered || 0} + </span> + </div> + <div className="flex justify-between text-sm"> + <span>URLs Processed:</span> + <span className="font-medium"> + {crawlProgress.processedUrls || crawlSession?.pages_processed || 0} + </span> + </div> + {getCurrentCrawlStatus() === "running" && ( + <div className="space-y-2"> + <div className="flex justify-between text-sm"> + <span>Progress:</span> + <span className="font-medium">{getCrawlProgress()}%</span> + </div> + <Progress value={getCrawlProgress()} className="w-full" /> + {crawlProgress.currentUrl && ( + <div className="text-xs text-gray-500 truncate"> + Currently processing: {crawlProgress.currentUrl} + </div> + )} + </div> + )} + </div> + )} + + <div className="flex gap-2 mt-4"> + <Button + onClick={startCrawl} + disabled={isLoading || getCurrentCrawlStatus() === "running"} + className="flex items-center gap-2" + > + <Play className="w-4 h-4" /> + {getCurrentCrawlStatus() === "running" + ? "Crawling..." + : "Start Crawl"} + </Button> + {getCurrentCrawlStatus() === "running" && ( + <Button + variant="outline" + onClick={stopCrawl} + disabled={isLoading} + className="flex items-center gap-2" + > + <Square className="w-4 h-4" /> + Stop + </Button> + )} + <Button + variant="outline" + onClick={() => { + loadPages(); + loadActiveCrawlSession(); + }} + disabled={isLoading} + className="flex items-center gap-2" + > + <RefreshCw + className={`w-4 h-4 ${isLoading ? "animate-spin" : ""}`} + /> + Refresh + </Button> + </div> + + {/* Status Messages */} + {getCurrentCrawlStatus() === "completed" && ( + <div className="mt-3 p-3 bg-green-50 border border-green-200 rounded-lg"> + <div className="flex items-center gap-2 text-green-800"> + <CheckCircle className="w-4 h-4" /> + <span className="text-sm font-medium">Crawl completed successfully!</span> + </div> + <p className="text-xs text-green-600 mt-1"> + {pages.length} pages discovered and ready for analysis. + </p> + </div> + )} + + {getCurrentCrawlStatus() === "failed" && ( + <div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg"> + <div className="flex items-center gap-2 text-red-800"> + <XCircle className="w-4 h-4" /> + <span className="text-sm font-medium">Crawl failed</span> + </div> + <p className="text-xs text-red-600 mt-1"> + {crawlSession?.error_message || "An error occurred during crawling."} + </p> + </div> + )} + </CardContent> + </Card> + + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between mb-4"> + <h3 className="text-lg font-semibold">Lighthouse Scanning</h3> + <Badge + className={ + isScanning + ? "bg-blue-100 text-blue-800" + : "bg-gray-100 text-gray-800" + } + > + {isScanning ? "Scanning..." : "Ready"} + </Badge> + </div> + + <div className="space-y-3"> + <div className="flex justify-between text-sm"> + <span>Active Pages:</span> + <span className="font-medium"> + {pages.filter((p) => p.is_active).length} + </span> + </div> + <div className="flex justify-between text-sm"> + <span>Total Pages:</span> + <span className="font-medium"> + {pages.filter((p) => p.is_active).length} + </span> + </div> + </div> + + <div className="flex gap-2 mt-4"> + <Button + onClick={startScanning} + disabled={isScanning || pages.filter((p) => p.is_active).length === 0} + className="flex items-center gap-2" + > + <BarChart3 className="w-4 h-4" /> + {isScanning ? "Scanning..." : "Start Scan"} + </Button> + <Button + variant="outline" + onClick={savePageSelection} + disabled={isLoading} + className="flex items-center gap-2" + > + <Settings className="w-4 h-4" /> + Save Selection + </Button> + </div> + </CardContent> + </Card> + </motion.div> + + {/* Page Management */} + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.2 }} + > + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-between mb-6"> + <div className="flex items-center gap-2"> + <Globe className="w-5 h-5 text-blue-600" /> + <h3 className="text-lg font-semibold"> + Discovered Pages ({filteredPages.length}) + </h3> + </div> + + <div className="flex items-center gap-2"> + <select + value={filterStatus} + onChange={(e) => setFilterStatus(e.target.value)} + className="px-3 py-1 border rounded-md text-sm" + > + <option value="all">All Pages</option> + <option value="active">Active Only</option> + <option value="inactive">Inactive Only</option> + </select> + <Button + variant="outline" + size="sm" + onClick={selectAll} + disabled={isLoading} + > + {isLoading ? "Selecting..." : "Select All"} + </Button> + <Button + variant="outline" + size="sm" + onClick={deselectAll} + disabled={isLoading} + > + {isLoading ? "Deselecting..." : "Deselect All"} + </Button> + </div> + </div> + + {isLoadingPages ? ( + <div className="text-center py-12"> + <div className="flex items-center justify-center gap-2 text-gray-500"> + <RefreshCw className="w-4 h-4 animate-spin" /> + <span>Loading pages...</span> + </div> + </div> + ) : filteredPages.length === 0 ? ( + <div className="text-center py-12"> + <Globe className="w-12 h-12 mx-auto text-gray-400 mb-4" /> + <p className="text-gray-600 mb-2">No pages discovered yet</p> + <p className="text-sm text-gray-500"> + Start a crawl to discover pages on your website + </p> + </div> + ) : ( + <div className="space-y-2 max-h-96 overflow-y-auto"> + <AnimatePresence> + {filteredPages.map((page, index) => ( + <motion.div + key={page.id} + initial={{ opacity: 0, x: -20 }} + animate={{ opacity: 1, x: 0 }} + exit={{ opacity: 0, x: 20 }} + transition={{ delay: index * 0.05 }} + className={`flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition-colors ${ + page.is_active ? "bg-blue-50 border-blue-200" : "" + }`} + > + <div className="flex items-center space-x-3 flex-1"> + <input + type="checkbox" + checked={page.is_active} + onChange={() => updatePageStatus(page.id, !page.is_active)} + className="rounded border-gray-300" + /> + {getPageStatusIcon(page)} + <div className="flex-1 min-w-0"> + <div className="flex items-center gap-2"> + <p className="font-medium text-gray-900 truncate"> + {page.title || "Untitled Page"} + </p> + {/* Depth entfernt */} + {!page.is_active ? ( + <Badge variant="red" className="text-xs"> + Inactive + </Badge> + ) : ( + <Badge variant="green" className="text-xs"> + Active + </Badge> + )} + </div> + <div className="flex items-center gap-2 mt-1"> + <p className="text-sm text-gray-500 truncate"> + {page.url} + </p> + <a + href={page.url} + target="_blank" + rel="noopener noreferrer" + className="text-blue-600 hover:text-blue-800" + > + <ExternalLink className="w-3 h-3" /> + </a> + </div> + </div> + </div> + + <div className="flex items-center space-x-3"> + {page.last_scan?.performance_score && ( + <Badge + className={`${ + page.last_scan.performance_score >= 90 + ? "bg-green-100 text-green-800" + : page.last_scan.performance_score >= 70 + ? "bg-yellow-100 text-yellow-800" + : "bg-red-100 text-red-800" + }`} + > + {page.last_scan.performance_score}% + </Badge> + )} + </div> + </motion.div> + ))} + </AnimatePresence> + </div> + )} + </CardContent> + </Card> + </motion.div> + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/dashboard/CriticalIssuesList.tsx b/website-monitoring-frontend/src/components/dashboard/CriticalIssuesList.tsx new file mode 100644 index 0000000..c46d443 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/CriticalIssuesList.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import type { CriticalIssue } from "@/types/metrics"; +import { ChevronDown, ChevronUp } from "lucide-react"; + +interface CriticalIssuesListProps { + issues: CriticalIssue[]; +} + +export const CriticalIssuesList = ({ issues }: CriticalIssuesListProps) => { + const [expandedIssue, setExpandedIssue] = useState<number | null>(null); + + return ( + <div className="space-y-4"> + <h3 className="text-lg font-semibold text-slate-800">Critical Issues</h3> + <div className="space-y-3"> + {issues.map((item, index) => ( + <motion.div + key={index} + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: index * 0.1 }} + > + <div className="rounded-lg overflow-hidden border border-gray-200 bg-white"> + <div + className="flex items-center justify-between p-3 bg-gray-50 hover:bg-gray-100 transition-colors duration-200 cursor-pointer" + onClick={() => + setExpandedIssue(expandedIssue === index ? null : index) + } + > + <div className="flex items-center gap-2"> + <span + className={`w-2 h-2 rounded-full ${ + item.severity === "high" + ? "bg-red-500" + : item.severity === "medium" + ? "bg-yellow-500" + : "bg-blue-500" + }`} + /> + <div className="flex flex-col"> + <span className="font-medium text-gray-900"> + {item.issue} + </span> + <span className="text-sm text-gray-500"> + {getSeverityLabel(item.severity)} + </span> + </div> + </div> + <div className="flex items-center gap-3"> + <span className="text-sm font-medium text-gray-600 bg-gray-100 px-2 py-1 rounded"> + {item.metric} + </span> + {expandedIssue === index ? ( + <ChevronUp className="h-5 w-5 text-gray-500" /> + ) : ( + <ChevronDown className="h-5 w-5 text-gray-500" /> + )} + </div> + </div> + <AnimatePresence> + {expandedIssue === index && ( + <motion.div + initial={{ height: 0, opacity: 0 }} + animate={{ height: "auto", opacity: 1 }} + exit={{ height: 0, opacity: 0 }} + transition={{ duration: 0.2 }} + className="border-t border-gray-200" + > + <div className="p-4 bg-white"> + <h4 className="font-medium text-gray-900 mb-2"> + Impact & Resolution + </h4> + <p className="text-gray-600 text-sm leading-relaxed"> + {getIssueDescription(item)} + </p> + {item.recommendation && ( + <div className="mt-3 text-sm text-blue-600"> + Recommendation: {item.recommendation} + </div> + )} + </div> + </motion.div> + )} + </AnimatePresence> + </div> + </motion.div> + ))} + </div> + </div> + ); +}; + +function getSeverityLabel(severity: string): string { + switch (severity) { + case "high": + return "High Priority"; + case "medium": + return "Medium Priority"; + case "low": + return "Low Priority"; + default: + return "Unknown Priority"; + } +} + +function getIssueDescription(issue: CriticalIssue): string { + const descriptions: Record<string, string> = { + "Largest Contentful Paint too slow": + "The largest element on your page takes too long to load (> 2.5s). This affects user experience and Core Web Vitals. Consider optimizing images, reducing server response time, and implementing lazy loading.", + "Render-blocking resources": + "Critical resources are blocking your page's first paint. To fix: defer non-critical JavaScript, inline critical CSS, and use async/defer attributes on scripts. This can significantly improve page load time.", + "Unused JavaScript": + "Your page includes JavaScript code that isn't being used. Implement code splitting, remove unused dependencies, and minimize bundle size to improve loading performance and reduce data usage.", + }; + + return ( + issue.description || + descriptions[issue.issue] || + "This issue affects your website's performance. Consider addressing it to improve user experience." + ); +} diff --git a/website-monitoring-frontend/src/components/dashboard/DeviceComparison.tsx b/website-monitoring-frontend/src/components/dashboard/DeviceComparison.tsx new file mode 100644 index 0000000..fd98ad8 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/DeviceComparison.tsx @@ -0,0 +1,72 @@ +import { Monitor, Smartphone } from "lucide-react"; +import { motion } from "framer-motion"; +import type { DeviceMetrics } from "@/types/metrics"; + +interface DeviceComparisonProps { + desktop: DeviceMetrics; + mobile: DeviceMetrics; +} + +const colorVariants = { + blue: "bg-blue-600", + purple: "bg-purple-600", +} as const; + +type ColorType = keyof typeof colorVariants; + +export const DeviceComparison = ({ + desktop, + mobile, +}: DeviceComparisonProps) => { + return ( + <div className="space-y-4"> + <h3 className="text-lg font-semibold text-slate-800"> + Device Comparison + </h3> + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> + {/* Desktop Metrics */} + <div> + <h4 className="text-md font-medium mb-3 flex items-center"> + <Monitor className="h-5 w-5 mr-2" /> Desktop + </h4> + <MetricsBars metrics={desktop} color="blue" /> + </div> + + {/* Mobile Metrics */} + <div> + <h4 className="text-md font-medium mb-3 flex items-center"> + <Smartphone className="h-5 w-5 mr-2" /> Mobile + </h4> + <MetricsBars metrics={mobile} color="purple" /> + </div> + </div> + </div> + ); +}; + +const MetricsBars = ({ + metrics, + color, +}: { + metrics: DeviceMetrics; + color: ColorType; +}) => ( + <div className="space-y-2"> + {Object.entries(metrics).map(([metric, value], i) => ( + <div key={i} className="flex justify-between items-center"> + <span className="text-sm text-gray-600">{metric}</span> + <div className="flex items-center"> + <div className="w-24 bg-gray-200 rounded-full h-2 mr-2"> + <motion.div + className={`${colorVariants[color]} h-2 rounded-full`} + initial={{ width: 0 }} + animate={{ width: `${value}%` }} + transition={{ duration: 0.5 }} + /> + </div> + <span className="text-sm font-medium">{value}</span> + </div> + </div> + ))} + </div> +); diff --git a/website-monitoring-frontend/src/components/dashboard/EnhancedWebsiteList.tsx b/website-monitoring-frontend/src/components/dashboard/EnhancedWebsiteList.tsx new file mode 100644 index 0000000..f9cfcc1 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/EnhancedWebsiteList.tsx @@ -0,0 +1,633 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { motion, AnimatePresence } from "framer-motion"; +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Badge } from "@/components/ui/layout/Badge"; +import { Checkbox } from "@/components/ui/forms/Checkbox"; +import { + Globe, + Plus, + Search, + Filter, + MoreVertical, + ExternalLink, + Activity, + BarChart3, + Settings, + Trash2, + RefreshCw, + CheckCircle, + XCircle, + Clock, + AlertTriangle, + TrendingUp, + TrendingDown, + Eye, + Play, + Pause, + Zap, + Loader2, + Check, +} from "lucide-react"; +import { useDashboardData } from "@/hooks/useDashboardData"; +import { websiteService } from "@/services/websiteService"; + +interface Website { + id: string; + name: string; + base_url: string; + is_active: boolean; + created_at: string; + scan_status?: string; + last_scan_at?: string; + stats: { + pagesCount: number; + latestScan: any | null; + }; +} + +interface FilterOptions { + status: "all" | "active" | "inactive"; + scanStatus: "all" | "scanned" | "unscanned" | "issues"; + sortBy: "name" | "created" | "performance" | "pages"; +} + +export function EnhancedWebsiteList() { + const router = useRouter(); + const { user, userDetails, organizationId, shouldShowLoading } = useDashboardData({ requireOrganization: false }); + const [websites, setWebsites] = useState<Website[]>([]); + const [filteredWebsites, setFilteredWebsites] = useState<Website[]>([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [showFilters, setShowFilters] = useState(false); + const [selectedWebsites, setSelectedWebsites] = useState<string[]>([]); + const [bulkLoading, setBulkLoading] = useState(false); + const [success, setSuccess] = useState(""); + const [error, setError] = useState(""); + const [filters, setFilters] = useState<FilterOptions>({ + status: "all", + scanStatus: "all", + sortBy: "created", + }); + + useEffect(() => { + if (userDetails && user) { + if (organizationId) { + loadWebsites(); + } else { + // No organization yet, show empty state + setWebsites([]); + setFilteredWebsites([]); + setLoading(false); + } + } + }, [userDetails, user, organizationId]); + + useEffect(() => { + applyFilters(); + }, [websites, searchTerm, filters]); + + const loadWebsites = async () => { + if (!organizationId || !user) { + setLoading(false); + return; + } + + try { + setLoading(true); + setError(""); // Clear any previous errors + const data = await websiteService.getWebsites(organizationId, user.id); + setWebsites(data || []); + console.log("Successfully loaded websites:", data?.length || 0); + } catch (error) { + console.error("Error loading websites:", error); + console.error("Error type:", typeof error); + console.error("Error constructor:", error?.constructor?.name); + + // Provide more specific error message + let errorMessage = "Failed to load websites"; + if (error instanceof Error) { + errorMessage = error.message; + } else if (typeof error === 'string') { + errorMessage = error; + } else if (error && typeof error === 'object') { + errorMessage = JSON.stringify(error); + } + + setError(errorMessage); + setWebsites([]); // Set empty array so UI doesn't break + } finally { + setLoading(false); + } + }; + + const applyFilters = () => { + let filtered = [...websites]; + + // Search filter + if (searchTerm) { + filtered = filtered.filter( + (website) => + website.name.toLowerCase().includes(searchTerm.toLowerCase()) || + website.base_url.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + // Status filter + if (filters.status !== "all") { + filtered = filtered.filter((website) => + filters.status === "active" ? website.is_active : !website.is_active + ); + } + + // Scan status filter + if (filters.scanStatus !== "all") { + filtered = filtered.filter((website) => { + switch (filters.scanStatus) { + case "scanned": + return website.stats.latestScan; + case "unscanned": + return !website.stats.latestScan; + case "issues": + return website.stats.latestScan?.lighthouse_score < 70; + default: + return true; + } + }); + } + + // Sort + filtered.sort((a, b) => { + switch (filters.sortBy) { + case "name": + return a.name.localeCompare(b.name); + case "created": + return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); + case "performance": + const aScore = a.stats.latestScan?.lighthouse_score || 0; + const bScore = b.stats.latestScan?.lighthouse_score || 0; + return bScore - aScore; + case "pages": + return b.stats.pagesCount - a.stats.pagesCount; + default: + return 0; + } + }); + + setFilteredWebsites(filtered); + }; + + const handleBulkAction = async (action: string) => { + if (selectedWebsites.length === 0) return; + + try { + setBulkLoading(true); + setError(""); + setSuccess(""); + + const response = await fetch("/api/websites/bulk", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action, + websiteIds: selectedWebsites, + userId: user?.id, + organizationId: userDetails?.organization_id, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Bulk operation failed"); + } + + setSuccess(data.message); + setSelectedWebsites([]); + await loadWebsites(); + } catch (error) { + console.error("Bulk operation error:", error); + setError(error instanceof Error ? error.message : "Bulk operation failed"); + } finally { + setBulkLoading(false); + } + }; + + const handleDeleteWebsite = async (websiteId: string) => { + if (!confirm("Are you sure you want to delete this website? This action cannot be undone.")) { + return; + } + + try { + await websiteService.deleteWebsite(websiteId, user!.id); + setSuccess("Website deleted successfully"); + await loadWebsites(); + } catch (error) { + console.error("Delete error:", error); + setError(error instanceof Error ? error.message : "Failed to delete website"); + } + }; + + const toggleWebsiteSelection = (websiteId: string) => { + setSelectedWebsites(prev => + prev.includes(websiteId) + ? prev.filter(id => id !== websiteId) + : [...prev, websiteId] + ); + }; + + const toggleSelectAll = () => { + setSelectedWebsites( + selectedWebsites.length === filteredWebsites.length + ? [] + : filteredWebsites.map(w => w.id) + ); + }; + + const getStatusColor = (website: Website) => { + if (!website.is_active) return "bg-gray-100 text-gray-800"; + if (website.scan_status === "running") return "bg-blue-100 text-blue-800"; + if (website.stats.latestScan) { + const score = website.stats.latestScan.lighthouse_score; + if (score >= 90) return "bg-green-100 text-green-800"; + if (score >= 70) return "bg-yellow-100 text-yellow-800"; + return "bg-red-100 text-red-800"; + } + return "bg-gray-100 text-gray-800"; + }; + + const getStatusIcon = (website: Website) => { + if (!website.is_active) return <Pause className="w-4 h-4" />; + if (website.scan_status === "running") return <Loader2 className="w-4 h-4 animate-spin" />; + if (website.stats.latestScan) { + const score = website.stats.latestScan.lighthouse_score; + if (score >= 90) return <CheckCircle className="w-4 h-4" />; + if (score >= 70) return <AlertTriangle className="w-4 h-4" />; + return <XCircle className="w-4 h-4" />; + } + return <Clock className="w-4 h-4" />; + }; + + const canManageWebsites = userDetails?.role === "owner" || userDetails?.role === "admin"; + + if (loading) { + return ( + <div className="flex items-center justify-center py-12"> + <Loader2 className="w-8 h-8 animate-spin text-blue-600" /> + </div> + ); + } + + return ( + <div className="space-y-6"> + {/* Success/Error Messages */} + <AnimatePresence> + {success && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + className="bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-3" + > + <Check className="w-5 h-5 text-green-500" /> + <span className="text-green-800">{success}</span> + <Button + variant="outline" + size="sm" + onClick={() => setSuccess("")} + className="ml-auto" + > + Dismiss + </Button> + </motion.div> + )} + + {error && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3" + > + <AlertTriangle className="w-5 h-5 text-red-500" /> + <span className="text-red-800">{error}</span> + <Button + variant="outline" + size="sm" + onClick={() => setError("")} + className="ml-auto" + > + Dismiss + </Button> + </motion.div> + )} + </AnimatePresence> + + {/* Header with Actions */} + <div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between"> + <div> + <h2 className="text-2xl font-bold text-gray-900"> + Websites ({filteredWebsites.length}) + </h2> + <p className="text-gray-600"> + Monitor and analyze your website performance + </p> + </div> + <div className="flex gap-2"> + <Button + variant="outline" + onClick={() => setShowFilters(!showFilters)} + className="flex items-center gap-2" + > + <Filter className="w-4 h-4" /> + Filters + </Button> + <Button + variant="outline" + onClick={loadWebsites} + disabled={refreshing} + className="flex items-center gap-2" + > + <RefreshCw className={`w-4 h-4 ${refreshing ? "animate-spin" : ""}`} /> + Refresh + </Button> + <Button + onClick={() => router.push("/dashboard/websites/new")} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + Add Website + </Button> + </div> + </div> + + {/* Search and Filters */} + <div className="space-y-4"> + <div className="relative"> + <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" /> + <input + type="text" + placeholder="Search websites..." + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + </div> + + {showFilters && ( + <motion.div + initial={{ opacity: 0, height: 0 }} + animate={{ opacity: 1, height: "auto" }} + exit={{ opacity: 0, height: 0 }} + className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg" + > + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Status + </label> + <select + value={filters.status} + onChange={(e) => setFilters(prev => ({ ...prev, status: e.target.value as any }))} + className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + > + <option value="all">All</option> + <option value="active">Active</option> + <option value="inactive">Inactive</option> + </select> + </div> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Scan Status + </label> + <select + value={filters.scanStatus} + onChange={(e) => setFilters(prev => ({ ...prev, scanStatus: e.target.value as any }))} + className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + > + <option value="all">All</option> + <option value="scanned">Scanned</option> + <option value="unscanned">Unscanned</option> + <option value="issues">Has Issues</option> + </select> + </div> + <div> + <label className="block text-sm font-medium text-gray-700 mb-1"> + Sort By + </label> + <select + value={filters.sortBy} + onChange={(e) => setFilters(prev => ({ ...prev, sortBy: e.target.value as any }))} + className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + > + <option value="created">Date Created</option> + <option value="name">Name</option> + <option value="performance">Performance</option> + <option value="pages">Pages</option> + </select> + </div> + </motion.div> + )} + </div> + + {/* Bulk Actions */} + {selectedWebsites.length > 0 && canManageWebsites && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + className="bg-blue-50 border border-blue-200 rounded-lg p-4" + > + <div className="flex items-center justify-between"> + <span className="text-blue-800 font-medium"> + {selectedWebsites.length} website(s) selected + </span> + <div className="flex gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => handleBulkAction("activate")} + disabled={bulkLoading} + className="flex items-center gap-1" + > + <Play className="w-3 h-3" /> + Activate + </Button> + <Button + variant="outline" + size="sm" + onClick={() => handleBulkAction("deactivate")} + disabled={bulkLoading} + className="flex items-center gap-1" + > + <Pause className="w-3 h-3" /> + Deactivate + </Button> + <Button + variant="outline" + size="sm" + onClick={() => handleBulkAction("scan")} + disabled={bulkLoading} + className="flex items-center gap-1" + > + <Zap className="w-3 h-3" /> + Scan + </Button> + <Button + variant="destructive" + size="sm" + onClick={() => handleBulkAction("delete")} + disabled={bulkLoading} + className="flex items-center gap-1" + > + {bulkLoading ? ( + <Loader2 className="w-3 h-3 animate-spin" /> + ) : ( + <Trash2 className="w-3 h-3" /> + )} + Delete + </Button> + </div> + </div> + </motion.div> + )} + + {/* Websites Grid */} + {filteredWebsites.length > 0 ? ( + <div className="space-y-4"> + {canManageWebsites && ( + <div className="flex items-center gap-2 pb-2 border-b border-gray-200"> + <Checkbox + checked={selectedWebsites.length === filteredWebsites.length && filteredWebsites.length > 0} + onCheckedChange={toggleSelectAll} + /> + <span className="text-sm text-gray-600"> + Select all + </span> + </div> + )} + + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> + <AnimatePresence> + {filteredWebsites.map((website) => ( + <motion.div + key={website.id} + initial={{ opacity: 0, scale: 0.95 }} + animate={{ opacity: 1, scale: 1 }} + exit={{ opacity: 0, scale: 0.95 }} + className="group" + > + <Card className="hover:shadow-lg transition-shadow duration-200"> + <CardContent className="p-6"> + <div className="flex items-start justify-between mb-4"> + <div className="flex items-start gap-3 flex-1"> + {canManageWebsites && ( + <Checkbox + checked={selectedWebsites.includes(website.id)} + onCheckedChange={() => toggleWebsiteSelection(website.id)} + className="mt-1" + /> + )} + <div className="flex-1 min-w-0"> + <h3 className="font-semibold text-gray-900 truncate"> + {website.name} + </h3> + <p className="text-sm text-gray-500 truncate"> + {website.base_url} + </p> + </div> + </div> + <div className="flex items-center gap-2"> + <Badge className={`text-xs ${getStatusColor(website)}`}> + <div className="flex items-center gap-1"> + {getStatusIcon(website)} + {!website.is_active + ? "Inactive" + : website.scan_status === "running" + ? "Scanning" + : website.stats.latestScan + ? `${website.stats.latestScan.lighthouse_score}/100` + : "Pending" + } + </div> + </Badge> + </div> + </div> + + <div className="grid grid-cols-2 gap-4 mb-4"> + <div className="text-center"> + <div className="text-lg font-semibold text-gray-900"> + {website.stats.pagesCount} + </div> + <div className="text-xs text-gray-500">Pages</div> + </div> + <div className="text-center"> + <div className="text-lg font-semibold text-gray-900"> + {website.stats.latestScan?.lighthouse_score || "—"} + </div> + <div className="text-xs text-gray-500">Score</div> + </div> + </div> + + <div className="flex gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => router.push(`/dashboard/websites/${website.id}`)} + className="flex-1" + > + <BarChart3 className="w-3 h-3 mr-1" /> + View + </Button> + <Button + variant="outline" + size="sm" + onClick={() => router.push(`/dashboard/websites/${website.id}/settings`)} + className="flex-1" + > + <Settings className="w-3 h-3 mr-1" /> + Settings + </Button> + {canManageWebsites && ( + <Button + variant="outline" + size="sm" + onClick={() => handleDeleteWebsite(website.id)} + className="text-red-600 hover:text-red-700 hover:bg-red-50" + > + <Trash2 className="w-3 h-3" /> + </Button> + )} + </div> + </CardContent> + </Card> + </motion.div> + ))} + </AnimatePresence> + </div> + </div> + ) : ( + <div className="text-center py-12"> + <Globe className="w-12 h-12 text-gray-400 mx-auto mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + No websites found + </h3> + <p className="text-gray-600 mb-6"> + {searchTerm || filters.status !== "all" || filters.scanStatus !== "all" + ? "Try adjusting your search or filters" + : "Get started by adding your first website"} + </p> + <Button + onClick={() => router.push("/dashboard/websites/new")} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + Add Website + </Button> + </div> + )} + </div> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/components/dashboard/IssueSeverityTooltip.tsx b/website-monitoring-frontend/src/components/dashboard/IssueSeverityTooltip.tsx new file mode 100644 index 0000000..4c11808 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/IssueSeverityTooltip.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { motion } from "framer-motion"; + +interface IssueSeverityTooltipProps { + severity: "high" | "medium" | "low"; + description: string; + isVisible: boolean; + position?: "left" | "right"; // Add position prop +} + +export const IssueSeverityTooltip = ({ + severity, + description, + isVisible, + position = "right", // Default to right +}: IssueSeverityTooltipProps) => { + const getSeverityColor = () => { + switch (severity) { + case "high": + return "bg-red-50 text-red-800 border-red-200"; + case "medium": + return "bg-yellow-50 text-yellow-800 border-yellow-200"; + case "low": + return "bg-blue-50 text-blue-800 border-blue-200"; + } + }; + + return ( + <motion.div + initial={{ opacity: 0, scale: 0.95 }} + animate={ + isVisible ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.95 } + } + transition={{ duration: 0.2 }} + className={`absolute z-50 p-3 rounded-lg border shadow-lg ${getSeverityColor()} + w-72 ${position === "right" ? "left-full ml-2" : "right-full mr-2"} + top-1/2 -translate-y-1/2`} + style={{ + pointerEvents: isVisible ? "auto" : "none", + visibility: isVisible ? "visible" : "hidden", + }} + > + {/* Add a tooltip arrow */} + <div + className={`absolute top-1/2 -translate-y-1/2 ${ + position === "right" + ? "left-0 -translate-x-1" + : "right-0 translate-x-1" + } w-2 h-2 rotate-45 ${getSeverityColor()}`} + /> + + <div className="font-medium capitalize mb-1"> + {severity} Severity Issue + </div> + <div className="text-sm opacity-90 leading-relaxed">{description}</div> + </motion.div> + ); +}; diff --git a/website-monitoring-frontend/src/components/dashboard/MetricsCard.tsx b/website-monitoring-frontend/src/components/dashboard/MetricsCard.tsx new file mode 100644 index 0000000..d122f38 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/MetricsCard.tsx @@ -0,0 +1,61 @@ +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { MetricsGauge as Gauge } from "@/components/ui/data-display/Gauge"; +import { TrendingUp, TrendingDown } from "lucide-react"; +import { motion } from "framer-motion"; + +interface MetricsCardProps { + title: string; + value: number; + previousValue: number; + isPercentage?: boolean; +} + +export const MetricsCard = ({ + title, + value, + previousValue, + isPercentage = false, +}: MetricsCardProps) => { + const trend = Math.round((value - previousValue) * 100) / 100; + + return ( + <Card className="overflow-hidden border-0 shadow-md"> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4 text-slate-800">{title}</h3> + <div className="flex items-center justify-between"> + <div className="flex justify-center items-center w-24 h-24"> + <Gauge value={isPercentage ? value / 100 : value} /> + </div> + <div className="flex flex-col items-end"> + <span className="text-2xl font-bold text-slate-800"> + {Math.round(isPercentage ? value : value * 100)} + </span> + {trend > 0 ? ( + <motion.div + className="flex items-center text-emerald-500 text-sm" + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + key={`${title}-${value}-up`} + > + <TrendingUp size={16} className="mr-1" /> + <span> + +{Math.abs(trend * (isPercentage ? 1 : 100)).toFixed(1)}% + </span> + </motion.div> + ) : ( + <motion.div + className="flex items-center text-rose-500 text-sm" + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + key={`${title}-${value}-down`} + > + <TrendingDown size={16} className="mr-1" /> + <span>{(trend * (isPercentage ? 1 : 100)).toFixed(1)}%</span> + </motion.div> + )} + </div> + </div> + </CardContent> + </Card> + ); +}; diff --git a/website-monitoring-frontend/src/components/dashboard/PerformanceChart.tsx b/website-monitoring-frontend/src/components/dashboard/PerformanceChart.tsx new file mode 100644 index 0000000..91d63bf --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/PerformanceChart.tsx @@ -0,0 +1,42 @@ +import { EnhancedChart } from "@/components/ui/data-display/EnhancedChart"; + +interface PerformanceChartProps { + performance?: number[]; + uptime?: number[]; + labels?: string[]; +} + +export const PerformanceChart = ({ + performance = [], + uptime = [], + labels = [], +}: PerformanceChartProps) => { + // If no data, show placeholder or loading state + if (!performance.length || !uptime.length || !labels.length) { + return ( + <div className="h-64 flex items-center justify-center"> + <p className="text-gray-500">No performance data available</p> + </div> + ); + } + + return ( + <EnhancedChart + datasets={[ + { + label: "Performance", + data: performance, + color: "rgba(56, 189, 248, 1)", + backgroundColor: "rgba(56, 189, 248, 0.1)", + }, + { + label: "Uptime", + data: uptime, + color: "rgba(139, 92, 246, 1)", + backgroundColor: "rgba(139, 92, 246, 0.1)", + }, + ]} + labels={labels} + /> + ); +}; diff --git a/website-monitoring-frontend/src/components/dashboard/ScanScheduleManager.tsx b/website-monitoring-frontend/src/components/dashboard/ScanScheduleManager.tsx new file mode 100644 index 0000000..c5c0476 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/ScanScheduleManager.tsx @@ -0,0 +1,504 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Badge } from "@/components/ui/layout/Badge"; +import { Checkbox } from "@/components/ui/forms/Checkbox"; +import { Label } from "@/components/ui/forms/Label"; +import { + Clock, + Zap, + Settings, + Play, + Pause, + AlertTriangle, + CheckCircle, + XCircle +} from "lucide-react"; +import { supabase } from "@/lib/supabase"; +import { logError } from "@/utils/errorUtils"; + +interface ScanSchedule { + frequency: 'hourly' | 'daily' | 'weekly' | 'monthly'; + deviceTypes: ('desktop' | 'mobile')[]; + categories: ('performance' | 'accessibility' | 'seo' | 'best_practices')[]; + isActive: boolean; +} + +interface SubscriptionLimits { + maxScansPerDay: number; + maxScansPerMonth: number; + changeDetectionEnabled: boolean; + scheduledScansEnabled: boolean; + scanFrequency: 'hourly' | 'daily' | 'weekly' | 'monthly'; +} + +interface ScanUsage { + daily: number; + monthly: number; +} + +interface Website { + id: string; + name: string; + base_url: string; + scan_schedule?: ScanSchedule; + last_scan_at?: string; + organizations: { + subscription_tier: string; + }[]; +} + +export function ScanScheduleManager({ websiteId }: { websiteId: string }) { + const [website, setWebsite] = useState<Website | null>(null); + const [schedule, setSchedule] = useState<ScanSchedule>({ + frequency: 'daily', + deviceTypes: ['desktop'], + categories: ['performance', 'accessibility', 'seo', 'best_practices'], + isActive: false, + }); + const [subscriptionLimits, setSubscriptionLimits] = useState<SubscriptionLimits | null>(null); + const [scanUsage, setScanUsage] = useState<ScanUsage>({ daily: 0, monthly: 0 }); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + loadWebsiteData(); + }, [websiteId]); + + const loadWebsiteData = async () => { + try { + setLoading(true); + + const { data: websiteData, error: websiteError } = await supabase + .from('websites') + .select(` + id, + name, + base_url, + scan_schedule, + last_scan_at, + organizations!inner ( + subscription_tier + ) + `) + .eq('id', websiteId) + .single(); + + if (websiteError) throw websiteError; + + setWebsite(websiteData as Website); + setSchedule(websiteData.scan_schedule || { + frequency: 'daily', + deviceTypes: ['desktop'], + categories: ['performance', 'accessibility', 'seo', 'best_practices'], + isActive: false, + }); + + const limits = getSubscriptionLimits(websiteData.organizations[0]?.subscription_tier || 'free'); + setSubscriptionLimits(limits); + + await loadScanUsage(websiteData.id); + + } catch (error) { + logError('Error loading website data', error, { websiteId }); + } finally { + setLoading(false); + } + }; + + const loadScanUsage = async (websiteId: string) => { + try { + const now = new Date(); + const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + const { data: dailyScans } = await supabase + .from('scans') + .select('id') + .eq('website_id', websiteId) + .gte('created_at', startOfDay.toISOString()); + + const { data: monthlyScans } = await supabase + .from('scans') + .select('id') + .eq('website_id', websiteId) + .gte('created_at', startOfMonth.toISOString()); + + setScanUsage({ + daily: dailyScans?.length || 0, + monthly: monthlyScans?.length || 0, + }); + } catch (error) { + logError('Error loading scan usage', error, { websiteId }); + } + }; + + const saveSchedule = async () => { + try { + setSaving(true); + + const { error } = await supabase + .from('websites') + .update({ + scan_schedule: schedule, + }) + .eq('id', websiteId); + + if (error) throw error; + + await loadWebsiteData(); + } catch (error) { + logError('Error saving scan schedule', error, { websiteId, schedule }); + } finally { + setSaving(false); + } + }; + + const toggleSchedule = () => { + setSchedule(prev => ({ ...prev, isActive: !prev.isActive })); + }; + + const updateFrequency = (frequency: ScanSchedule['frequency']) => { + setSchedule(prev => ({ ...prev, frequency })); + }; + + const toggleDeviceType = (deviceType: 'desktop' | 'mobile') => { + setSchedule(prev => ({ + ...prev, + deviceTypes: prev.deviceTypes.includes(deviceType) + ? prev.deviceTypes.filter(d => d !== deviceType) + : [...prev.deviceTypes, deviceType], + })); + }; + + const toggleCategory = (category: ScanSchedule['categories'][0]) => { + setSchedule(prev => ({ + ...prev, + categories: prev.categories.includes(category) + ? prev.categories.filter(c => c !== category) + : [...prev.categories, category], + })); + }; + + const triggerManualScan = async () => { + try { + const response = await fetch('/api/cron/scan', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + websiteId, + pageId: website?.id, + deviceType: 'desktop', + categories: schedule.categories, + priority: 'high', + }), + }); + + if (!response.ok) { + throw new Error(`Scan failed: ${response.statusText}`); + } + + await loadWebsiteData(); + } catch (error) { + logError('Error triggering manual scan', error, { websiteId }); + } + }; + + const getSubscriptionLimits = (tier: string): SubscriptionLimits => { + switch (tier) { + case 'free': + return { + maxScansPerDay: 5, + maxScansPerMonth: 50, + changeDetectionEnabled: false, + scheduledScansEnabled: false, + scanFrequency: 'weekly', + }; + case 'starter': + return { + maxScansPerDay: 20, + maxScansPerMonth: 200, + changeDetectionEnabled: true, + scheduledScansEnabled: true, + scanFrequency: 'daily', + }; + case 'professional': + return { + maxScansPerDay: 100, + maxScansPerMonth: 1000, + changeDetectionEnabled: true, + scheduledScansEnabled: true, + scanFrequency: 'hourly', + }; + case 'enterprise': + return { + maxScansPerDay: 500, + maxScansPerMonth: 5000, + changeDetectionEnabled: true, + scheduledScansEnabled: true, + scanFrequency: 'hourly', + }; + default: + return { + maxScansPerDay: 5, + maxScansPerMonth: 50, + changeDetectionEnabled: false, + scheduledScansEnabled: false, + scanFrequency: 'weekly', + }; + } + }; + + if (loading) { + return ( + <Card> + <CardContent className="p-6"> + <div className="flex items-center justify-center"> + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> + </div> + </CardContent> + </Card> + ); + } + + if (!website || !subscriptionLimits) { + return ( + <Card> + <CardContent className="p-6"> + <div className="text-center text-gray-500"> + <XCircle className="h-8 w-8 mx-auto mb-2" /> + <p>Website not found or subscription information unavailable</p> + </div> + </CardContent> + </Card> + ); + } + + const canUseScheduledScans = subscriptionLimits.scheduledScansEnabled; + const canUseChangeDetection = subscriptionLimits.changeDetectionEnabled; + const isDailyLimitReached = scanUsage.daily >= subscriptionLimits.maxScansPerDay; + const isMonthlyLimitReached = scanUsage.monthly >= subscriptionLimits.maxScansPerMonth; + + return ( + <div className="space-y-6"> + {/* Subscription Status */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Settings className="h-5 w-5" /> + Subscription & Usage + </CardTitle> + </CardHeader> + <CardContent> + <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> + <div className="space-y-2"> + <Label className="text-sm font-medium">Subscription Tier</Label> + <Badge variant={website.organizations[0]?.subscription_tier === 'free' ? 'secondary' : 'default'}> + {website.organizations[0]?.subscription_tier || 'free'} + </Badge> + </div> + <div className="space-y-2"> + <Label className="text-sm font-medium">Daily Usage</Label> + <div className="flex items-center gap-2"> + <span className="text-sm"> + {scanUsage.daily} / {subscriptionLimits.maxScansPerDay} + </span> + {isDailyLimitReached ? ( + <XCircle className="h-4 w-4 text-red-500" /> + ) : ( + <CheckCircle className="h-4 w-4 text-green-500" /> + )} + </div> + </div> + <div className="space-y-2"> + <Label className="text-sm font-medium">Monthly Usage</Label> + <div className="flex items-center gap-2"> + <span className="text-sm"> + {scanUsage.monthly} / {subscriptionLimits.maxScansPerMonth} + </span> + {isMonthlyLimitReached ? ( + <XCircle className="h-4 w-4 text-red-500" /> + ) : ( + <CheckCircle className="h-4 w-4 text-green-500" /> + )} + </div> + </div> + </div> + + <div className="mt-4 space-y-2"> + <div className="flex items-center gap-2"> + {canUseScheduledScans ? ( + <CheckCircle className="h-4 w-4 text-green-500" /> + ) : ( + <XCircle className="h-4 w-4 text-red-500" /> + )} + <span className="text-sm">Scheduled Scans</span> + </div> + <div className="flex items-center gap-2"> + {canUseChangeDetection ? ( + <CheckCircle className="h-4 w-4 text-green-500" /> + ) : ( + <XCircle className="h-4 w-4 text-red-500" /> + )} + <span className="text-sm">Change Detection</span> + </div> + </div> + </CardContent> + </Card> + + {/* Scan Schedule Configuration */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Clock className="h-5 w-5" /> + Automatic Scan Schedule + </CardTitle> + </CardHeader> + <CardContent> + {!canUseScheduledScans ? ( + <div className="text-center py-8"> + <AlertTriangle className="h-12 w-12 mx-auto mb-4 text-yellow-500" /> + <h3 className="text-lg font-semibold mb-2">Scheduled Scans Not Available</h3> + <p className="text-gray-600 mb-4"> + Upgrade your subscription to enable automatic scheduled scans. + </p> + <Button variant="outline">Upgrade Subscription</Button> + </div> + ) : ( + <div className="space-y-6"> + <div className="flex items-center justify-between"> + <div> + <Label className="text-base font-medium">Enable Automatic Scans</Label> + <p className="text-sm text-gray-600"> + Automatically scan your website based on the schedule below + </p> + </div> + <Button + variant={schedule.isActive ? "default" : "outline"} + onClick={toggleSchedule} + disabled={saving} + > + {schedule.isActive ? ( + <> + <Pause className="h-4 w-4 mr-2" /> + Pause + </> + ) : ( + <> + <Play className="h-4 w-4 mr-2" /> + Start + </> + )} + </Button> + </div> + + <div className="space-y-3"> + <Label className="text-sm font-medium">Scan Frequency</Label> + <div className="grid grid-cols-2 md:grid-cols-4 gap-2"> + {(['hourly', 'daily', 'weekly', 'monthly'] as const).map((freq) => ( + <Button + key={freq} + variant={schedule.frequency === freq ? "default" : "outline"} + size="sm" + onClick={() => updateFrequency(freq)} + disabled={saving} + > + {freq.charAt(0).toUpperCase() + freq.slice(1)} + </Button> + ))} + </div> + </div> + + <div className="space-y-3"> + <Label className="text-sm font-medium">Device Types</Label> + <div className="space-y-2"> + {(['desktop', 'mobile'] as const).map((device) => ( + <div key={device} className="flex items-center space-x-2"> + <Checkbox + id={`device-${device}`} + checked={schedule.deviceTypes.includes(device)} + onCheckedChange={() => toggleDeviceType(device)} + disabled={saving} + /> + <Label htmlFor={`device-${device}`} className="capitalize"> + {device} + </Label> + </div> + ))} + </div> + </div> + + <div className="space-y-3"> + <Label className="text-sm font-medium">Scan Categories</Label> + <div className="grid grid-cols-2 gap-2"> + {(['performance', 'accessibility', 'seo', 'best_practices'] as const).map((category) => ( + <div key={category} className="flex items-center space-x-2"> + <Checkbox + id={`category-${category}`} + checked={schedule.categories.includes(category)} + onCheckedChange={() => toggleCategory(category)} + disabled={saving} + /> + <Label htmlFor={`category-${category}`} className="capitalize"> + {category.replace('_', ' ')} + </Label> + </div> + ))} + </div> + </div> + + <div className="flex justify-end"> + <Button onClick={saveSchedule} disabled={saving}> + {saving ? "Saving..." : "Save Schedule"} + </Button> + </div> + </div> + )} + </CardContent> + </Card> + + {/* Manual Scan */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Zap className="h-5 w-5" /> + Manual Scan + </CardTitle> + </CardHeader> + <CardContent> + <div className="flex items-center justify-between"> + <div> + <Label className="text-base font-medium">Trigger Manual Scan</Label> + <p className="text-sm text-gray-600"> + Run a scan immediately with current settings + </p> + </div> + <Button + onClick={triggerManualScan} + disabled={isDailyLimitReached || isMonthlyLimitReached} + > + <Zap className="h-4 w-4 mr-2" /> + Scan Now + </Button> + </div> + + {(isDailyLimitReached || isMonthlyLimitReached) && ( + <div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg"> + <div className="flex items-center gap-2"> + <AlertTriangle className="h-4 w-4 text-yellow-600" /> + <span className="text-sm text-yellow-800"> + {isDailyLimitReached + ? "Daily scan limit reached. Try again tomorrow or upgrade your subscription." + : "Monthly scan limit reached. Upgrade your subscription for more scans." + } + </span> + </div> + </div> + )} + </CardContent> + </Card> + </div> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/components/dashboard/TeamManagement.tsx b/website-monitoring-frontend/src/components/dashboard/TeamManagement.tsx new file mode 100644 index 0000000..9504b2a --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/TeamManagement.tsx @@ -0,0 +1,453 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useAuth } from "@/contexts/AuthContext"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Input } from "@/components/ui/forms/Input"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/components/ui/forms/Form"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Users, + UserPlus, + Mail, + Crown, + Shield, + User, + Trash2, + Settings, + Loader2, + AlertCircle, + Check, +} from "lucide-react"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { motion, AnimatePresence } from "framer-motion"; + +interface TeamMember { + id: string; + name: string; + email: string; + role: "owner" | "admin" | "member"; + created_at: string; +} + +const inviteFormSchema = z.object({ + email: z.string().email("Please enter a valid email address"), + role: z.enum(["admin", "member"], { + required_error: "Please select a role", + }), +}); + +interface TeamManagementProps { + organizationId: string; +} + +export function TeamManagement({ organizationId }: TeamManagementProps) { + const [members, setMembers] = useState<TeamMember[]>([]); + const [loading, setLoading] = useState(true); + const [inviting, setInviting] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const { user, userDetails } = useAuth(); + + const inviteForm = useForm<z.infer<typeof inviteFormSchema>>({ + resolver: zodResolver(inviteFormSchema), + defaultValues: { + email: "", + role: "member", + }, + }); + + useEffect(() => { + if (organizationId && user) { + loadMembers(); + } + }, [organizationId, user]); + + const loadMembers = async () => { + try { + setLoading(true); + const response = await fetch( + `/api/organization/members?organizationId=${organizationId}&userId=${user?.id}` + ); + + if (!response.ok) { + throw new Error("Failed to load members"); + } + + const data = await response.json(); + setMembers(data.members || []); + } catch (error) { + console.error("Error loading members:", error); + setError("Failed to load team members"); + } finally { + setLoading(false); + } + }; + + const handleInviteMember = async (values: z.infer<typeof inviteFormSchema>) => { + try { + setInviting(true); + setError(""); + setSuccess(""); + + const response = await fetch("/api/organization/members", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + organizationId, + email: values.email, + role: values.role, + invitedBy: user?.id, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to invite member"); + } + + if (data.member) { + // User was added immediately (existing user) + setMembers(prev => [...prev, data.member]); + setSuccess("Team member added successfully!"); + } else { + // Invitation was sent to new user + setSuccess("Invitation sent! The user will be added when they accept."); + } + + inviteForm.reset(); + } catch (error) { + console.error("Error inviting member:", error); + setError(error instanceof Error ? error.message : "Failed to invite member"); + } finally { + setInviting(false); + } + }; + + const handleUpdateRole = async (memberId: string, newRole: string) => { + try { + const response = await fetch("/api/organization/members", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + memberId, + role: newRole, + updatedBy: user?.id, + organizationId, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to update role"); + } + + // Update local state + setMembers(prev => + prev.map(member => + member.id === memberId + ? { ...member, role: newRole as TeamMember["role"] } + : member + ) + ); + + setSuccess("Member role updated successfully!"); + } catch (error) { + console.error("Error updating role:", error); + setError(error instanceof Error ? error.message : "Failed to update role"); + } + }; + + const handleRemoveMember = async (memberId: string) => { + if (!confirm("Are you sure you want to remove this member from the organization?")) { + return; + } + + try { + const response = await fetch( + `/api/organization/members?memberId=${memberId}&removedBy=${user?.id}&organizationId=${organizationId}`, + { + method: "DELETE", + } + ); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to remove member"); + } + + // Update local state + setMembers(prev => prev.filter(member => member.id !== memberId)); + setSuccess("Member removed successfully!"); + } catch (error) { + console.error("Error removing member:", error); + setError(error instanceof Error ? error.message : "Failed to remove member"); + } + }; + + const getRoleIcon = (role: string) => { + switch (role) { + case "owner": + return <Crown className="w-4 h-4 text-yellow-500" />; + case "admin": + return <Shield className="w-4 h-4 text-blue-500" />; + default: + return <User className="w-4 h-4 text-gray-500" />; + } + }; + + const getRoleBadgeColor = (role: string) => { + switch (role) { + case "owner": + return "bg-yellow-100 text-yellow-800"; + case "admin": + return "bg-blue-100 text-blue-800"; + default: + return "bg-gray-100 text-gray-800"; + } + }; + + const canManageMembers = userDetails?.role === "owner" || userDetails?.role === "admin"; + const canUpdateRoles = userDetails?.role === "owner"; + + if (loading) { + return ( + <Card> + <CardContent className="flex items-center justify-center py-8"> + <Loader2 className="w-6 h-6 animate-spin text-blue-600" /> + </CardContent> + </Card> + ); + } + + return ( + <div className="space-y-6"> + {/* Invite New Member */} + {canManageMembers && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <UserPlus className="w-5 h-5 text-blue-600" /> + Invite Team Member + </CardTitle> + </CardHeader> + <CardContent> + <Form {...inviteForm}> + <form + onSubmit={inviteForm.handleSubmit(handleInviteMember)} + className="space-y-4" + > + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <FormField + control={inviteForm.control} + name="email" + render={({ field }) => ( + <FormItem> + <FormLabel>Email Address</FormLabel> + <FormControl> + <div className="relative"> + <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" /> + <Input + {...field} + type="email" + placeholder="user@example.com" + className="pl-10" + disabled={inviting} + /> + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={inviteForm.control} + name="role" + render={({ field }) => ( + <FormItem> + <FormLabel>Role</FormLabel> + <FormControl> + <select + {...field} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + disabled={inviting} + > + <option value="member">Member</option> + <option value="admin">Admin</option> + </select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <Button type="submit" disabled={inviting} className="w-full md:w-auto"> + {inviting ? ( + <> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + Inviting... + </> + ) : ( + <> + <UserPlus className="w-4 h-4 mr-2" /> + Invite Member + </> + )} + </Button> + </form> + </Form> + </CardContent> + </Card> + )} + + {/* Team Members List */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Users className="w-5 h-5 text-blue-600" /> + Team Members ({members.length}) + </CardTitle> + </CardHeader> + <CardContent> + {/* Success/Error Messages */} + <AnimatePresence> + {success && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + className="bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-3 mb-4" + > + <Check className="w-5 h-5 text-green-500" /> + <span className="text-green-800">{success}</span> + <Button + variant="outline" + size="sm" + onClick={() => setSuccess("")} + className="ml-auto" + > + Dismiss + </Button> + </motion.div> + )} + + {error && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3 mb-4" + > + <AlertCircle className="w-5 h-5 text-red-500" /> + <span className="text-red-800">{error}</span> + <Button + variant="outline" + size="sm" + onClick={() => setError("")} + className="ml-auto" + > + Dismiss + </Button> + </motion.div> + )} + </AnimatePresence> + + <div className="space-y-3"> + {members.map((member) => ( + <motion.div + key={member.id} + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + className="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors" + > + <div className="flex items-center gap-3"> + <div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center"> + <User className="w-5 h-5 text-blue-600" /> + </div> + <div> + <div className="flex items-center gap-2"> + <span className="font-medium text-gray-900"> + {member.name || member.email} + </span> + {getRoleIcon(member.role)} + </div> + <div className="flex items-center gap-2 text-sm text-gray-500"> + <span>{member.email}</span> + <Badge className={`text-xs ${getRoleBadgeColor(member.role)}`}> + {member.role.charAt(0).toUpperCase() + member.role.slice(1)} + </Badge> + </div> + <span className="text-xs text-gray-400"> + Joined {new Date(member.created_at).toLocaleDateString()} + </span> + </div> + </div> + + <div className="flex items-center gap-2"> + {/* Role Update Dropdown */} + {canUpdateRoles && member.role !== "owner" && ( + <select + value={member.role} + onChange={(e) => handleUpdateRole(member.id, e.target.value)} + className="text-sm px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + > + <option value="member">Member</option> + <option value="admin">Admin</option> + </select> + )} + + {/* Remove Member Button */} + {canManageMembers && member.role !== "owner" && member.id !== user?.id && ( + <Button + variant="outline" + size="sm" + onClick={() => handleRemoveMember(member.id)} + className="text-red-600 hover:text-red-700 hover:bg-red-50" + > + <Trash2 className="w-4 h-4" /> + </Button> + )} + </div> + </motion.div> + ))} + </div> + + {members.length === 0 && ( + <div className="text-center py-8"> + <Users className="w-12 h-12 text-gray-400 mx-auto mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + No Team Members + </h3> + <p className="text-gray-600"> + Invite team members to collaborate on your organization + </p> + </div> + )} + </CardContent> + </Card> + </div> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/components/dashboard/TimelineView.tsx b/website-monitoring-frontend/src/components/dashboard/TimelineView.tsx new file mode 100644 index 0000000..e463827 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/TimelineView.tsx @@ -0,0 +1,71 @@ +import { motion } from "framer-motion"; +import type { TimelineEvent } from "@/types/metrics"; +import { cn } from "@/lib/utils"; + +interface TimelineViewProps { + events: TimelineEvent[]; +} + +export const TimelineView = ({ events }: TimelineViewProps) => { + const maxTime = Math.max(...events.map((event) => event.time)); + + return ( + <div className="space-y-4"> + <h3 className="text-lg font-semibold text-slate-800"> + Page Load Timeline + </h3> + <div className="space-y-3"> + {/* Timeline markers */} + <div className="relative h-16"> + {events.map((event, i) => ( + <motion.div + key={i} + className="absolute bottom-0 flex flex-col items-center" + style={{ left: `${(event.time / maxTime) * 100}%` }} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ + delay: i * 0.1, + duration: 0.3, + ease: "easeOut", + }} + > + <div className={cn("w-1 h-8 mb-1", event.color)}></div> + <span className="text-xs font-medium whitespace-nowrap"> + {event.name} + </span> + <span className="text-xs text-gray-500"> + {event.time.toFixed(1)}s + </span> + </motion.div> + ))} + <div className="absolute bottom-8 left-0 right-0 h-0.5 bg-gray-200"></div> + </div> + + {/* Timeline bars */} + <div className="space-y-2"> + {events.map((event, i) => ( + <div key={i} className="space-y-1"> + <div className="flex justify-between text-sm"> + <span className="font-medium">{event.name}</span> + <span className="text-gray-500">{event.time.toFixed(1)}s</span> + </div> + <div className="h-2 bg-gray-100 rounded-full overflow-hidden"> + <motion.div + className={cn("h-full", event.color)} + initial={{ width: 0 }} + animate={{ width: `${(event.time / maxTime) * 100}%` }} + transition={{ + delay: i * 0.1, + duration: 0.5, + ease: "easeOut", + }} + /> + </div> + </div> + ))} + </div> + </div> + </div> + ); +}; diff --git a/website-monitoring-frontend/src/components/dashboard/WebsiteList.tsx b/website-monitoring-frontend/src/components/dashboard/WebsiteList.tsx new file mode 100644 index 0000000..06b1d30 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/WebsiteList.tsx @@ -0,0 +1,706 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { motion, AnimatePresence } from "framer-motion"; +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Globe, + Plus, + Search, + Filter, + MoreVertical, + ExternalLink, + Activity, + BarChart3, + Settings, + Trash2, + RefreshCw, + CheckCircle, + XCircle, + Clock, + AlertTriangle, + TrendingUp, + TrendingDown, + Eye, +} from "lucide-react"; +import { supabase } from "@/lib/supabase"; +import { scanService } from "@/services/scanService"; +import { safeDecodeHtmlEntities } from "@/utils/htmlUtils"; + +interface Website { + id: string; + name: string; + base_url: string; + is_active: boolean; + created_at: string; + pages_count: number; + active_pages_count: number; + last_scan_at?: string; + last_scan_status?: string; + average_performance?: number; + crawl_status?: string; + issues_count?: number; +} + +interface FilterOptions { + status: "all" | "active" | "inactive"; + scanStatus: "all" | "scanned" | "unscanned" | "issues"; + sortBy: "name" | "created" | "performance" | "pages"; +} + +export function WebsiteList() { + const router = useRouter(); + const [websites, setWebsites] = useState<Website[]>([]); + const [filteredWebsites, setFilteredWebsites] = useState<Website[]>([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [showFilters, setShowFilters] = useState(false); + const [filters, setFilters] = useState<FilterOptions>({ + status: "all", + scanStatus: "all", + sortBy: "created", + }); + const [selectedWebsites, setSelectedWebsites] = useState<string[]>([]); + const [scanProgress, setScanProgress] = useState<{ [key: string]: boolean }>( + {}, + ); + + useEffect(() => { + loadWebsites(); + }, []); + + useEffect(() => { + applyFilters(); + }, [websites, searchTerm, filters]); + + const loadWebsites = async () => { + try { + setLoading(true); + + // Fetch websites with related data + const { data: websitesData, error } = await supabase + .from("websites") + .select( + ` + id, + name, + base_url, + is_active, + created_at, + pages!inner ( + id, + is_active, + scans ( + id, + status, + created_at, + scan_results ( + category, + score + ) + ) + ), + crawl_sessions ( + id, + status, + created_at + ) + `, + ) + .order("created_at", { ascending: false }); + + if (error) throw error; + + // Process the data to calculate statistics + const processedWebsites: Website[] = + websitesData?.map((website: any) => { + const pages = website.pages || []; + const activePages = pages.filter((p: any) => p.is_active); + const allScans = pages.flatMap((p: any) => p.scans || []); + const completedScans = allScans.filter( + (s: any) => s.status === "completed", + ); + const latestScan = allScans.sort( + (a: any, b: any) => + new Date(b.created_at).getTime() - + new Date(a.created_at).getTime(), + )[0]; + + const averagePerformance = + completedScans.length > 0 + ? Math.round( + completedScans.reduce( + (sum: number, scan: any) => + sum + ((scan.scan_results || []).find((r: any) => r.category === "performance")?.score || 0), + 0, + ) / completedScans.length, + ) + : undefined; + + const latestCrawlSession = website.crawl_sessions?.sort( + (a: any, b: any) => + new Date(b.created_at).getTime() - + new Date(a.created_at).getTime(), + )[0]; + + return { + id: website.id, + name: website.name, + base_url: website.base_url, + is_active: website.is_active, + created_at: website.created_at, + pages_count: pages.length, + active_pages_count: activePages.length, + last_scan_at: latestScan?.created_at, + last_scan_status: latestScan?.status, + average_performance: averagePerformance, + crawl_status: latestCrawlSession?.status, + issues_count: (allScans.filter((s: any) => s.status === "failed").length) || 0, + }; + }) || []; + + setWebsites(processedWebsites.map(w => ({ ...w, issues_count: w.issues_count ?? 0 }))); + } catch (error) { + console.error("Failed to load websites:", error); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + const applyFilters = () => { + let filtered = [...websites]; + + // Apply search filter + if (searchTerm) { + const term = searchTerm.toLowerCase(); + filtered = filtered.filter( + (website) => + safeDecodeHtmlEntities(website.name).toLowerCase().includes(term) || + website.base_url.toLowerCase().includes(term), + ); + } + + // Apply status filter + if (filters.status !== "all") { + filtered = filtered.filter((website) => + filters.status === "active" ? website.is_active : !website.is_active, + ); + } + + // Apply scan status filter + if (filters.scanStatus !== "all") { + filtered = filtered.filter((website) => { + switch (filters.scanStatus) { + case "scanned": + return website.last_scan_at; + case "unscanned": + return !website.last_scan_at; + case "issues": + return (website.issues_count || 0) > 0; + default: + return true; + } + }); + } + + // Apply sorting + filtered.sort((a, b) => { + switch (filters.sortBy) { + case "name": + return a.name.localeCompare(b.name); + case "performance": + return (b.average_performance || 0) - (a.average_performance || 0); + case "pages": + return b.active_pages_count - a.active_pages_count; + case "created": + default: + return ( + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + } + }); + + setFilteredWebsites(filtered); + }; + + const handleRefresh = async () => { + setRefreshing(true); + await loadWebsites(); + }; + + const handleScanWebsite = async (websiteId: string) => { + try { + setScanProgress((prev) => ({ ...prev, [websiteId]: true })); + const result = await scanService.scanWebsite(websiteId); + + if (result.success) { + // Monitor scan progress + const monitorProgress = async () => { + if (result.scanIds) { + const statusMap = await scanService.monitorScanProgress( + result.scanIds, + ); + const allCompleted = Object.values(statusMap).every( + (status) => status === "completed" || status === "failed", + ); + + if (allCompleted) { + setScanProgress((prev) => ({ ...prev, [websiteId]: false })); + await loadWebsites(); + } else { + setTimeout(monitorProgress, 3000); + } + } + }; + + monitorProgress(); + } else { + setScanProgress((prev) => ({ ...prev, [websiteId]: false })); + alert("Failed to start scan: " + result.error); + } + } catch (error) { + setScanProgress((prev) => ({ ...prev, [websiteId]: false })); + console.error("Failed to start scan:", error); + } + }; + + const getStatusIcon = (website: Website) => { + if (scanProgress[website.id]) { + return <Activity className="w-4 h-4 text-blue-600 animate-pulse" />; + } + + if (!website.last_scan_at) { + return <Clock className="w-4 h-4 text-gray-400" />; + } + + switch (website.last_scan_status) { + case "completed": + return <CheckCircle className="w-4 h-4 text-green-600" />; + case "failed": + return <XCircle className="w-4 h-4 text-red-600" />; + case "running": + return <Activity className="w-4 h-4 text-blue-600 animate-pulse" />; + default: + return <Clock className="w-4 h-4 text-gray-400" />; + } + }; + + const getPerformanceBadge = (score?: number) => { + if (!score) return null; + + const color = + score >= 90 + ? "bg-green-100 text-green-800" + : score >= 70 + ? "bg-yellow-100 text-yellow-800" + : "bg-red-100 text-red-800"; + + return ( + <Badge className={color}> + <TrendingUp className="w-3 h-3 mr-1" /> + {score}% + </Badge> + ); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + }; + + if (loading) { + return ( + <div className="flex items-center justify-center h-64"> + <div className="flex items-center space-x-2"> + <RefreshCw className="w-6 h-6 animate-spin text-blue-600" /> + <span className="text-gray-600">Loading websites...</span> + </div> + </div> + ); + } + + return ( + <div className="space-y-6"> + {/* Header */} + <div className="flex items-center justify-between"> + <div> + <h1 className="text-2xl font-bold text-gray-900">Your Websites</h1> + <p className="text-gray-600 mt-1"> + Monitor and analyze your website performance + </p> + </div> + <div className="flex gap-3"> + <Button + variant="outline" + onClick={handleRefresh} + disabled={refreshing} + className="flex items-center gap-2" + > + <RefreshCw + className={`w-4 h-4 ${refreshing ? "animate-spin" : ""}`} + /> + Refresh + </Button> + <Button + onClick={() => router.push("/dashboard/websites/new")} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + Add Website + </Button> + </div> + </div> + + {/* Search and Filters */} + <Card> + <CardContent className="p-4"> + <div className="flex flex-col sm:flex-row gap-4"> + <div className="relative flex-1"> + <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> + <input + type="text" + placeholder="Search websites..." + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + </div> + + <div className="flex gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => setShowFilters(!showFilters)} + className="flex items-center gap-2" + > + <Filter className="w-4 h-4" /> + Filters + </Button> + + <select + value={filters.sortBy} + onChange={(e) => + setFilters((prev) => ({ + ...prev, + sortBy: e.target.value as FilterOptions["sortBy"], + })) + } + className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent" + > + <option value="created">Sort by Date</option> + <option value="name">Sort by Name</option> + <option value="performance">Sort by Performance</option> + <option value="pages">Sort by Pages</option> + </select> + </div> + </div> + + <AnimatePresence> + {showFilters && ( + <motion.div + initial={{ opacity: 0, height: 0 }} + animate={{ opacity: 1, height: "auto" }} + exit={{ opacity: 0, height: 0 }} + className="mt-4 pt-4 border-t border-gray-200" + > + <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> + <div> + <label className="block text-sm font-medium text-gray-700 mb-2"> + Website Status + </label> + <select + value={filters.status} + onChange={(e) => + setFilters((prev) => ({ + ...prev, + status: e.target.value as FilterOptions["status"], + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + > + <option value="all">All Websites</option> + <option value="active">Active Only</option> + <option value="inactive">Inactive Only</option> + </select> + </div> + + <div> + <label className="block text-sm font-medium text-gray-700 mb-2"> + Scan Status + </label> + <select + value={filters.scanStatus} + onChange={(e) => + setFilters((prev) => ({ + ...prev, + scanStatus: e.target + .value as FilterOptions["scanStatus"], + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + > + <option value="all">All</option> + <option value="scanned">Recently Scanned</option> + <option value="unscanned">Never Scanned</option> + <option value="issues">Has Issues</option> + </select> + </div> + </div> + </motion.div> + )} + </AnimatePresence> + </CardContent> + </Card> + + {/* Statistics */} + <div className="grid grid-cols-1 sm:grid-cols-4 gap-4"> + <Card> + <CardContent className="p-4"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Total Websites</p> + <p className="text-2xl font-bold">{websites.length}</p> + </div> + <Globe className="w-8 h-8 text-blue-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-4"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Active</p> + <p className="text-2xl font-bold text-green-600"> + {websites.filter((w) => w.is_active).length} + </p> + </div> + <CheckCircle className="w-8 h-8 text-green-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-4"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Total Pages</p> + <p className="text-2xl font-bold"> + {websites.reduce((sum, w) => sum + w.active_pages_count, 0)} + </p> + </div> + <Eye className="w-8 h-8 text-purple-600" /> + </div> + </CardContent> + </Card> + + <Card> + <CardContent className="p-4"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600">Avg Performance</p> + <p className="text-2xl font-bold text-blue-600"> + {Math.round( + websites + .filter((w) => w.average_performance) + .reduce( + (sum, w) => sum + (w.average_performance || 0), + 0, + ) / + Math.max( + 1, + websites.filter((w) => w.average_performance).length, + ), + ) || 0} + % + </p> + </div> + <BarChart3 className="w-8 h-8 text-orange-600" /> + </div> + </CardContent> + </Card> + </div> + + {/* Website Grid */} + {filteredWebsites.length === 0 ? ( + <Card> + <CardContent className="p-12 text-center"> + <Globe className="w-16 h-16 mx-auto text-gray-400 mb-4" /> + <h3 className="text-lg font-semibold text-gray-900 mb-2"> + {websites.length === 0 + ? "No websites added yet" + : "No websites match your filters"} + </h3> + <p className="text-gray-600 mb-6"> + {websites.length === 0 + ? "Add your first website to start monitoring its performance" + : "Try adjusting your search or filter criteria"} + </p> + {websites.length === 0 && ( + <Button + onClick={() => router.push("/dashboard/websites/new")} + className="flex items-center gap-2" + > + <Plus className="w-4 h-4" /> + Add Your First Website + </Button> + )} + </CardContent> + </Card> + ) : ( + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> + <AnimatePresence> + {filteredWebsites.map((website, index) => ( + <motion.div + key={website.id} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -20 }} + transition={{ delay: index * 0.1 }} + > + <Card + className={`hover:shadow-lg transition-all duration-200 cursor-pointer ${ + !website.is_active ? "opacity-75" : "" + }`} + onClick={() => + router.push(`/dashboard/websites/${website.id}`) + } + > + <CardContent className="p-6"> + <div className="flex items-start justify-between mb-4"> + <div className="flex items-center space-x-3"> + <div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center"> + <Globe className="w-5 h-5 text-blue-600" /> + </div> + <div> + <h3 className="font-semibold text-gray-900 truncate max-w-40"> + {safeDecodeHtmlEntities(website.name)} + </h3> + <div className="flex items-center gap-2 mt-1"> + <Badge + className={ + website.is_active + ? "bg-green-100 text-green-800" + : "bg-gray-100 text-gray-800" + } + > + {website.is_active ? "Active" : "Inactive"} + </Badge> + {(website.issues_count ?? 0) > 0 && ( + <Badge className="bg-red-100 text-red-800"> + <AlertTriangle className="w-3 h-3 mr-1" /> + {website.issues_count ?? 0} + </Badge> + )} + </div> + </div> + </div> + + <div className="flex items-center space-x-2"> + {getStatusIcon(website)} + <button + onClick={(e) => { + e.stopPropagation(); + // Handle dropdown menu + }} + className="p-1 rounded hover:bg-gray-100" + > + <MoreVertical className="w-4 h-4 text-gray-400" /> + </button> + </div> + </div> + + <div className="space-y-3"> + <div className="flex items-center justify-between text-sm"> + <span className="text-gray-600">URL:</span> + <div className="flex items-center gap-1"> + <span className="truncate max-w-32 text-blue-600"> + {website.base_url} + </span> + <a + href={website.base_url} + target="_blank" + rel="noopener noreferrer" + onClick={(e) => e.stopPropagation()} + className="text-blue-600 hover:text-blue-800" + > + <ExternalLink className="w-3 h-3" /> + </a> + </div> + </div> + + <div className="flex items-center justify-between text-sm"> + <span className="text-gray-600">Pages:</span> + <span className="font-medium"> + {website.active_pages_count} active + </span> + </div> + + {website.average_performance && ( + <div className="flex items-center justify-between text-sm"> + <span className="text-gray-600">Performance:</span> + {getPerformanceBadge(website.average_performance)} + </div> + )} + + <div className="flex items-center justify-between text-sm"> + <span className="text-gray-600">Added:</span> + <span className="text-gray-500"> + {formatDate(website.created_at)} + </span> + </div> + + {website.last_scan_at && ( + <div className="flex items-center justify-between text-sm"> + <span className="text-gray-600">Last Scan:</span> + <span className="text-gray-500"> + {formatDate(website.last_scan_at)} + </span> + </div> + )} + </div> + + <div className="flex gap-2 mt-4 pt-4 border-t border-gray-100"> + <Button + variant="outline" + size="sm" + onClick={(e) => { + e.stopPropagation(); + router.push(`/dashboard/websites/${website.id}`); + }} + className="flex-1 flex items-center justify-center gap-2" + > + <Settings className="w-3 h-3" /> + Manage + </Button> + <Button + size="sm" + onClick={(e) => { + e.stopPropagation(); + handleScanWebsite(website.id); + }} + disabled={scanProgress[website.id]} + className="flex-1 flex items-center justify-center gap-2" + > + <BarChart3 className="w-3 h-3" /> + {scanProgress[website.id] ? "Scanning..." : "Scan"} + </Button> + </div> + </CardContent> + </Card> + </motion.div> + ))} + </AnimatePresence> + </div> + )} + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/dashboard/WebsiteScanButton.tsx b/website-monitoring-frontend/src/components/dashboard/WebsiteScanButton.tsx new file mode 100644 index 0000000..404c9cb --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/WebsiteScanButton.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/forms/Button"; + +export function WebsiteScanButton({ url }: { url: string }) { + const [result, setResult] = useState<any>(null); + const [loading, setLoading] = useState(false); + + const runScan = async () => { + setLoading(true); + setResult(null); + const res = await fetch("http://localhost:5001/lighthouse", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url }), + }); + const data = await res.json(); + setResult(data); + setLoading(false); + }; + + return ( + <div className="mt-4"> + <Button + variant="outline" + size="sm" + onClick={(e) => { + e.stopPropagation(); + runScan(); + }} + disabled={loading} + > + {loading ? "Scan läuft..." : "Lighthouse-Scan"} + </Button> + {result && ( + <pre className="text-xs mt-2 bg-gray-100 p-2 rounded max-w-xl overflow-x-auto"> + {result.error + ? `Fehler: ${result.error}` + : JSON.stringify(result.categories, null, 2)} + </pre> + )} + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/dashboard/WebsiteSettings.tsx b/website-monitoring-frontend/src/components/dashboard/WebsiteSettings.tsx new file mode 100644 index 0000000..69dbd23 --- /dev/null +++ b/website-monitoring-frontend/src/components/dashboard/WebsiteSettings.tsx @@ -0,0 +1,791 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, +} from "@/components/ui/forms/Form"; +import { Input } from "@/components/ui/forms/Input"; +import { Button } from "@/components/ui/forms/Button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/layout/Card"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent, +} from "@/components/ui/layout/Tabs"; +import { websiteService } from "@/services/websiteService"; +import { supabase } from "@/lib/supabase"; +import { + Settings, + Save, + Clock, + Globe, + Search, + Bot, + Bell, + Mail, + MessageSquare, + Zap, + Filter, + RotateCcw, + CheckCircle, + AlertTriangle, + Info, + Loader2, + FileText, +} from "lucide-react"; + +const scanSettingsSchema = z.object({ + scanInterval: z + .number() + .min(5, "Minimum 5 minutes") + .max(1440, "Maximum 24 hours"), + maxPages: z + .number() + .min(1, "Minimum 1 page") + .max(10000, "Maximum 10,000 pages"), + maxDepth: z.number().min(1, "Minimum depth 1").max(10, "Maximum depth 10"), + userAgent: z.string().optional(), + excludePatterns: z.array(z.string()), + includePatterns: z.array(z.string()), + respectRobotsTxt: z.boolean(), + followRedirects: z.boolean(), + maxConcurrentRequests: z.number().min(1).max(10), +}); + +const alertSettingsSchema = z.object({ + performanceThreshold: z.number().min(0).max(100), + seoThreshold: z.number().min(0).max(100), + accessibilityThreshold: z.number().min(0).max(100), + uptimeThreshold: z.number().min(0).max(100), + notificationEmail: z.string().email().optional().or(z.literal("")), + slackWebhook: z.string().url().optional().or(z.literal("")), + enableEmailAlerts: z.boolean(), + enableSlackAlerts: z.boolean(), + alertFrequency: z.enum(["immediate", "hourly", "daily"]), +}); + +export function WebsiteSettings({ websiteId }: { websiteId: string }) { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [activeTab, setActiveTab] = useState("general"); + const [saveSuccess, setSaveSuccess] = useState<string | null>(null); + const [saveError, setSaveError] = useState<string | null>(null); + + const scanForm = useForm({ + resolver: zodResolver(scanSettingsSchema), + defaultValues: { + scanInterval: 60, + maxPages: 100, + maxDepth: 3, + userAgent: "", + excludePatterns: ["/admin/*", "/api/*"], + includePatterns: ["/*"], + respectRobotsTxt: true, + followRedirects: true, + maxConcurrentRequests: 3, + }, + }); + + const alertForm = useForm({ + resolver: zodResolver(alertSettingsSchema), + defaultValues: { + performanceThreshold: 90, + seoThreshold: 90, + accessibilityThreshold: 90, + uptimeThreshold: 99, + notificationEmail: "", + slackWebhook: "", + enableEmailAlerts: true, + enableSlackAlerts: false, + alertFrequency: "immediate" as const, + }, + }); + + const loadSettings = useCallback(async () => { + try { + setLoading(true); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) throw new Error("No authenticated user"); + + const settings = await websiteService.getWebsiteSettings(websiteId, user.id); + scanForm.reset(settings.scan); + alertForm.reset(settings.alerts); + } catch (error) { + console.error("Failed to load settings:", error); + setSaveError("Failed to load settings"); + } finally { + setLoading(false); + } + }, [websiteId, scanForm, alertForm]); + + useEffect(() => { + loadSettings(); + }, [loadSettings]); + + const showSuccessMessage = (message: string) => { + setSaveSuccess(message); + setSaveError(null); + setTimeout(() => setSaveSuccess(null), 3000); + }; + + const showErrorMessage = (message: string) => { + setSaveError(message); + setSaveSuccess(null); + }; + + const onSaveScanSettings = async ( + data: z.infer<typeof scanSettingsSchema>, + ) => { + try { + setSaving(true); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) throw new Error("No authenticated user"); + + await websiteService.updateScanSettings(websiteId, data, user.id); + showSuccessMessage("Scan settings saved successfully!"); + } catch (error) { + console.error("Failed to save scan settings:", error); + showErrorMessage("Failed to save scan settings"); + } finally { + setSaving(false); + } + }; + + const onSaveAlertSettings = async ( + data: z.infer<typeof alertSettingsSchema>, + ) => { + try { + setSaving(true); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) throw new Error("No authenticated user"); + + await websiteService.updateAlertSettings(websiteId, data, user.id); + showSuccessMessage("Alert settings saved successfully!"); + } catch (error) { + console.error("Failed to save alert settings:", error); + showErrorMessage("Failed to save alert settings"); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( + <div className="flex items-center justify-center p-12"> + <div className="text-center"> + <Loader2 className="h-8 w-8 animate-spin text-blue-600 mx-auto mb-4" /> + <p className="text-gray-600">Loading settings...</p> + </div> + </div> + ); + } + + return ( + <div className="space-y-6"> + {/* Header */} + <div className="flex items-center justify-between"> + <div> + <h2 className="text-3xl font-bold text-gray-900 flex items-center gap-3"> + <Settings className="w-8 h-8 text-blue-600" /> + Website Settings + </h2> + <p className="text-gray-600 mt-2"> + Configure how your website is monitored and when you receive alerts + </p> + </div> + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="sm" + onClick={loadSettings} + disabled={loading} + > + <RotateCcw className="w-4 h-4 mr-2" /> + Refresh + </Button> + </div> + </div> + + {/* Success/Error Messages */} + {saveSuccess && ( + <div className="bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-3"> + <CheckCircle className="w-5 h-5 text-green-600" /> + <span className="text-green-800">{saveSuccess}</span> + </div> + )} + + {saveError && ( + <div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3"> + <AlertTriangle className="w-5 h-5 text-red-600" /> + <span className="text-red-800">{saveError}</span> + </div> + )} + + <Tabs value={activeTab} onValueChange={setActiveTab}> + <TabsList className="grid w-full grid-cols-3"> + <TabsTrigger value="general" className="flex items-center gap-2"> + <Globe className="w-4 h-4" /> + General + </TabsTrigger> + <TabsTrigger value="scanning" className="flex items-center gap-2"> + <Search className="w-4 h-4" /> + Scanning + </TabsTrigger> + <TabsTrigger value="alerts" className="flex items-center gap-2"> + <Bell className="w-4 h-4" /> + Alerts + </TabsTrigger> + </TabsList> + + <TabsContent value="general" className="space-y-6"> + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Clock className="w-5 h-5 text-blue-600" /> + Scan Frequency + </CardTitle> + </CardHeader> + <CardContent> + <Form {...scanForm}> + <form + onSubmit={scanForm.handleSubmit(onSaveScanSettings)} + className="space-y-6" + > + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> + <FormField + control={scanForm.control} + name="scanInterval" + render={({ field }) => ( + <FormItem> + <FormLabel className="flex items-center gap-2"> + <Clock className="w-4 h-4" /> + Scan Interval (minutes) + </FormLabel> + <FormControl> + <Input + type="number" + {...field} + onChange={(e) => + field.onChange(parseInt(e.target.value)) + } + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + How often should we automatically scan your website? + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={scanForm.control} + name="maxPages" + render={({ field }) => ( + <FormItem> + <FormLabel className="flex items-center gap-2"> + <FileText className="w-4 h-4" /> + Maximum Pages + </FormLabel> + <FormControl> + <Input + type="number" + {...field} + onChange={(e) => + field.onChange(parseInt(e.target.value)) + } + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + Maximum number of pages to scan per session + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <div className="flex justify-end"> + <Button type="submit" disabled={saving}> + {saving ? ( + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + ) : ( + <Save className="w-4 h-4 mr-2" /> + )} + {saving ? "Saving..." : "Save General Settings"} + </Button> + </div> + </form> + </Form> + </CardContent> + </Card> + </TabsContent> + + <TabsContent value="scanning" className="space-y-6"> + <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> + {/* Crawler Configuration */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Bot className="w-5 h-5 text-green-600" /> + Crawler Configuration + </CardTitle> + </CardHeader> + <CardContent> + <Form {...scanForm}> + <form + onSubmit={scanForm.handleSubmit(onSaveScanSettings)} + className="space-y-4" + > + <FormField + control={scanForm.control} + name="maxDepth" + render={({ field }) => ( + <FormItem> + <FormLabel>Maximum Crawl Depth</FormLabel> + <FormControl> + <Input + type="number" + {...field} + onChange={(e) => + field.onChange(parseInt(e.target.value)) + } + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + How deep should the crawler go? (1-10 levels) + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={scanForm.control} + name="maxConcurrentRequests" + render={({ field }) => ( + <FormItem> + <FormLabel>Concurrent Requests</FormLabel> + <FormControl> + <Input + type="number" + {...field} + onChange={(e) => + field.onChange(parseInt(e.target.value)) + } + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + Number of simultaneous requests (1-10) + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={scanForm.control} + name="userAgent" + render={({ field }) => ( + <FormItem> + <FormLabel>Custom User Agent</FormLabel> + <FormControl> + <Input + {...field} + placeholder="Mozilla/5.0 (default if empty)" + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + Custom user agent string for requests + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <div className="flex justify-end pt-4"> + <Button type="submit" disabled={saving} size="sm"> + {saving ? ( + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + ) : ( + <Save className="w-4 h-4 mr-2" /> + )} + Save Crawler Settings + </Button> + </div> + </form> + </Form> + </CardContent> + </Card> + + {/* URL Filtering */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Filter className="w-5 h-5 text-purple-600" /> + URL Filtering + </CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> + <div className="flex items-start gap-3"> + <Info className="w-5 h-5 text-blue-600 mt-0.5" /> + <div> + <p className="text-sm font-medium text-blue-900"> + Pattern Matching + </p> + <p className="text-sm text-blue-700 mt-1"> + Use wildcards (*) to match multiple URLs. For example: + /admin/* matches all admin pages. + </p> + </div> + </div> + </div> + + <div className="space-y-3"> + <div> + <label className="text-sm font-medium text-gray-700 mb-2 block"> + Exclude Patterns + </label> + <div className="space-y-2"> + {["/admin/*", "/api/*", "/private/*"].map( + (pattern, index) => ( + <div + key={index} + className="flex items-center justify-between bg-gray-50 p-3 rounded" + > + <code className="text-sm">{pattern}</code> + <Badge variant="gray">Active</Badge> + </div> + ), + )} + </div> + </div> + + <div> + <label className="text-sm font-medium text-gray-700 mb-2 block"> + Include Patterns + </label> + <div className="bg-gray-50 p-3 rounded"> + <code className="text-sm">{"/*"}</code> + <span className="text-sm text-gray-600 ml-2"> + All pages included + </span> + </div> + </div> + </div> + </CardContent> + </Card> + </div> + </TabsContent> + + <TabsContent value="alerts" className="space-y-6"> + <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> + {/* Threshold Settings */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Zap className="w-5 h-5 text-yellow-600" /> + Performance Thresholds + </CardTitle> + </CardHeader> + <CardContent> + <Form {...alertForm}> + <form + onSubmit={alertForm.handleSubmit(onSaveAlertSettings)} + className="space-y-4" + > + <FormField + control={alertForm.control} + name="performanceThreshold" + render={({ field }) => ( + <FormItem> + <FormLabel className="flex items-center justify-between"> + <span>Performance Score</span> + <Badge variant="blue">{field.value}%</Badge> + </FormLabel> + <FormControl> + <Input + type="number" + {...field} + onChange={(e) => + field.onChange(parseInt(e.target.value)) + } + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + Alert when performance score drops below this value + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={alertForm.control} + name="seoThreshold" + render={({ field }) => ( + <FormItem> + <FormLabel className="flex items-center justify-between"> + <span>SEO Score</span> + <Badge variant="green">{field.value}%</Badge> + </FormLabel> + <FormControl> + <Input + type="number" + {...field} + onChange={(e) => + field.onChange(parseInt(e.target.value)) + } + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + Alert when SEO score drops below this value + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={alertForm.control} + name="accessibilityThreshold" + render={({ field }) => ( + <FormItem> + <FormLabel className="flex items-center justify-between"> + <span>Accessibility Score</span> + <Badge variant="purple">{field.value}%</Badge> + </FormLabel> + <FormControl> + <Input + type="number" + {...field} + onChange={(e) => + field.onChange(parseInt(e.target.value)) + } + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + Alert when accessibility score drops below this + value + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={alertForm.control} + name="uptimeThreshold" + render={({ field }) => ( + <FormItem> + <FormLabel className="flex items-center justify-between"> + <span>Uptime Percentage</span> + <Badge variant="green">{field.value}%</Badge> + </FormLabel> + <FormControl> + <Input + type="number" + {...field} + onChange={(e) => + field.onChange(parseInt(e.target.value)) + } + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + Alert when uptime drops below this percentage + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <div className="flex justify-end pt-4"> + <Button type="submit" disabled={saving} size="sm"> + {saving ? ( + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + ) : ( + <Save className="w-4 h-4 mr-2" /> + )} + Save Thresholds + </Button> + </div> + </form> + </Form> + </CardContent> + </Card> + + {/* Notification Settings */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Bell className="w-5 h-5 text-red-600" /> + Notification Channels + </CardTitle> + </CardHeader> + <CardContent> + <Form {...alertForm}> + <form + onSubmit={alertForm.handleSubmit(onSaveAlertSettings)} + className="space-y-4" + > + <div className="space-y-4"> + <div className="flex items-center gap-3 p-3 bg-blue-50 rounded-lg"> + <Mail className="w-5 h-5 text-blue-600" /> + <div className="flex-1"> + <p className="font-medium text-blue-900"> + Email Notifications + </p> + <p className="text-sm text-blue-700"> + Get alerts via email + </p> + </div> + <Badge + variant={ + alertForm.watch("enableEmailAlerts") + ? "green" + : "gray" + } + > + {alertForm.watch("enableEmailAlerts") + ? "Enabled" + : "Disabled"} + </Badge> + </div> + + <FormField + control={alertForm.control} + name="notificationEmail" + render={({ field }) => ( + <FormItem> + <FormLabel>Email Address</FormLabel> + <FormControl> + <Input + type="email" + {...field} + placeholder="alerts@example.com" + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + Email address for receiving alerts + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg"> + <MessageSquare className="w-5 h-5 text-green-600" /> + <div className="flex-1"> + <p className="font-medium text-green-900"> + Slack Notifications + </p> + <p className="text-sm text-green-700"> + Get alerts in Slack + </p> + </div> + <Badge + variant={ + alertForm.watch("enableSlackAlerts") + ? "green" + : "gray" + } + > + {alertForm.watch("enableSlackAlerts") + ? "Enabled" + : "Disabled"} + </Badge> + </div> + + <FormField + control={alertForm.control} + name="slackWebhook" + render={({ field }) => ( + <FormItem> + <FormLabel>Slack Webhook URL</FormLabel> + <FormControl> + <Input + {...field} + placeholder="https://hooks.slack.com/services/..." + className="bg-gray-50 focus:bg-white" + /> + </FormControl> + <FormDescription> + Slack webhook URL for sending alerts + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={alertForm.control} + name="alertFrequency" + render={({ field }) => ( + <FormItem> + <FormLabel>Alert Frequency</FormLabel> + <FormControl> + <select + {...field} + className="w-full p-2 border border-gray-300 rounded-md bg-gray-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500" + > + <option value="immediate">Immediate</option> + <option value="hourly">Hourly Digest</option> + <option value="daily">Daily Digest</option> + </select> + </FormControl> + <FormDescription> + How often should we send alert notifications? + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <div className="flex justify-end pt-4"> + <Button type="submit" disabled={saving} size="sm"> + {saving ? ( + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + ) : ( + <Save className="w-4 h-4 mr-2" /> + )} + Save Notifications + </Button> + </div> + </form> + </Form> + </CardContent> + </Card> + </div> + </TabsContent> + </Tabs> + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/debug/CrawlDebugger.tsx b/website-monitoring-frontend/src/components/debug/CrawlDebugger.tsx new file mode 100644 index 0000000..e5c75db --- /dev/null +++ b/website-monitoring-frontend/src/components/debug/CrawlDebugger.tsx @@ -0,0 +1,479 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Button } from "@/components/ui/forms/Button"; +import { Card, CardContent } from "@/components/ui/layout/Card"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + RefreshCw, + AlertCircle, + CheckCircle, + Clock, + XCircle, + Globe, + Database, + Activity, +} from "lucide-react"; + +interface QueueItem { + id: string; + url: string; + status: string; + discovery_depth: number; + attempts: number; + error_message?: string; +} + +interface Session { + id: string; + status: string; + pages_discovered: number; + pages_processed: number; + started_at: string; + error_message?: string; +} + +interface Page { + id: string; + url: string; + title?: string; + is_active: boolean; + depth: number; + created_at: string; +} + +interface Website { + name: string; + base_url: string; + crawl_settings?: { + max_depth?: number; + include_patterns?: string[]; + }; +} + +interface DebugData { + website: Website; + queueStats: { + total: number; + pending: number; + processing: number; + completed: number; + failed: number; + skipped: number; + }; + sessionStats: { + total: number; + running: number; + completed: number; + failed: number; + }; + pageStats: { + total: number; + active: number; + inactive: number; + }; + queueItems: QueueItem[]; + sessions: Session[]; + pages: Page[]; +} + +interface CrawlDebuggerProps { + websiteId: string; +} + +export function CrawlDebugger({ websiteId }: CrawlDebuggerProps) { + const [data, setData] = useState<DebugData | null>(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + const [autoRefresh, setAutoRefresh] = useState(false); + + const fetchDebugData = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(`/api/debug/crawl-queue/${websiteId}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const debugData = await response.json(); + setData(debugData); + } catch (err) { + console.error("Error fetching debug data:", err); + setError(err instanceof Error ? err.message : "Unknown error"); + } finally { + setLoading(false); + } + }, [websiteId]); + + useEffect(() => { + fetchDebugData(); + }, [fetchDebugData]); + + useEffect(() => { + let interval: NodeJS.Timeout; + if (autoRefresh) { + interval = setInterval(fetchDebugData, 5000); // Refresh every 5 seconds + } + return () => { + if (interval) clearInterval(interval); + }; + }, [autoRefresh, fetchDebugData]); + + const getStatusBadge = (status: string) => { + const statusConfig = { + pending: { color: "bg-yellow-100 text-yellow-800", icon: Clock }, + processing: { color: "bg-blue-100 text-blue-800", icon: Activity }, + completed: { color: "bg-green-100 text-green-800", icon: CheckCircle }, + failed: { color: "bg-red-100 text-red-800", icon: XCircle }, + skipped: { color: "bg-gray-100 text-gray-800", icon: AlertCircle }, + running: { color: "bg-blue-100 text-blue-800", icon: Activity }, + }; + + const config = statusConfig[status as keyof typeof statusConfig] || { + color: "bg-gray-100 text-gray-800", + icon: AlertCircle, + }; + const Icon = config.icon; + + return ( + <Badge className={config.color}> + <Icon className="w-3 h-3 mr-1" /> + {status} + </Badge> + ); + }; + + if (error) { + return ( + <Card> + <CardContent className="p-6"> + <div className="flex items-center text-red-600"> + <AlertCircle className="h-5 w-5 mr-2" /> + <span>Error: {error}</span> + </div> + <Button onClick={fetchDebugData} className="mt-4"> + Retry + </Button> + </CardContent> + </Card> + ); + } + + return ( + <div className="space-y-6"> + <div className="flex justify-between items-center"> + <h2 className="text-2xl font-bold">Crawl Debugger</h2> + <div className="flex gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => setAutoRefresh(!autoRefresh)} + > + <RefreshCw + className={`h-4 w-4 mr-2 ${autoRefresh ? "animate-spin" : ""}`} + /> + {autoRefresh ? "Auto Refresh ON" : "Auto Refresh OFF"} + </Button> + <Button + variant="outline" + size="sm" + onClick={fetchDebugData} + disabled={loading} + > + <RefreshCw + className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} + /> + Refresh + </Button> + </div> + </div> + + {data && ( + <> + {/* Website Info */} + <Card> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4 flex items-center"> + <Globe className="h-5 w-5 mr-2" /> + Website Information + </h3> + <div className="grid grid-cols-2 gap-4"> + <div> + <p className="text-sm text-gray-500">Name</p> + <p className="font-medium">{data.website.name}</p> + </div> + <div> + <p className="text-sm text-gray-500">Base URL</p> + <p className="font-medium">{data.website.base_url}</p> + </div> + <div> + <p className="text-sm text-gray-500">Max Depth</p> + <p className="font-medium"> + {data.website.crawl_settings?.max_depth || 3} + </p> + </div> + <div> + <p className="text-sm text-gray-500">Include Patterns</p> + <p className="font-medium"> + {JSON.stringify( + data.website.crawl_settings?.include_patterns || ["/*"], + )} + </p> + </div> + </div> + </CardContent> + </Card> + + {/* Statistics */} + <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> + {/* Queue Stats */} + <Card> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4 flex items-center"> + <Database className="h-5 w-5 mr-2" /> + Crawl Queue + </h3> + <div className="space-y-2"> + <div className="flex justify-between"> + <span>Total:</span> + <span className="font-medium">{data.queueStats.total}</span> + </div> + <div className="flex justify-between"> + <span>Pending:</span> + <span className="font-medium text-yellow-600"> + {data.queueStats.pending} + </span> + </div> + <div className="flex justify-between"> + <span>Processing:</span> + <span className="font-medium text-blue-600"> + {data.queueStats.processing} + </span> + </div> + <div className="flex justify-between"> + <span>Completed:</span> + <span className="font-medium text-green-600"> + {data.queueStats.completed} + </span> + </div> + <div className="flex justify-between"> + <span>Failed:</span> + <span className="font-medium text-red-600"> + {data.queueStats.failed} + </span> + </div> + <div className="flex justify-between"> + <span>Skipped:</span> + <span className="font-medium text-gray-600"> + {data.queueStats.skipped} + </span> + </div> + </div> + </CardContent> + </Card> + + {/* Session Stats */} + <Card> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4 flex items-center"> + <Activity className="h-5 w-5 mr-2" /> + Sessions + </h3> + <div className="space-y-2"> + <div className="flex justify-between"> + <span>Total:</span> + <span className="font-medium"> + {data.sessionStats.total} + </span> + </div> + <div className="flex justify-between"> + <span>Running:</span> + <span className="font-medium text-blue-600"> + {data.sessionStats.running} + </span> + </div> + <div className="flex justify-between"> + <span>Completed:</span> + <span className="font-medium text-green-600"> + {data.sessionStats.completed} + </span> + </div> + <div className="flex justify-between"> + <span>Failed:</span> + <span className="font-medium text-red-600"> + {data.sessionStats.failed} + </span> + </div> + </div> + </CardContent> + </Card> + + {/* Page Stats */} + <Card> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4 flex items-center"> + <Globe className="h-5 w-5 mr-2" /> + Discovered Pages + </h3> + <div className="space-y-2"> + <div className="flex justify-between"> + <span>Total:</span> + <span className="font-medium">{data.pageStats.total}</span> + </div> + <div className="flex justify-between"> + <span>Active:</span> + <span className="font-medium text-green-600"> + {data.pageStats.active} + </span> + </div> + <div className="flex justify-between"> + <span>Inactive:</span> + <span className="font-medium text-gray-600"> + {data.pageStats.inactive} + </span> + </div> + </div> + </CardContent> + </Card> + </div> + + {/* Recent Queue Items */} + <Card> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4">Recent Queue Items</h3> + <div className="overflow-x-auto"> + <table className="w-full text-sm"> + <thead> + <tr className="border-b"> + <th className="text-left p-2">URL</th> + <th className="text-left p-2">Status</th> + <th className="text-left p-2">Depth</th> + <th className="text-left p-2">Attempts</th> + <th className="text-left p-2">Error</th> + </tr> + </thead> + <tbody> + {data.queueItems.map((item) => ( + <tr key={item.id} className="border-b"> + <td className="p-2 max-w-xs truncate" title={item.url}> + {item.url} + </td> + <td className="p-2">{getStatusBadge(item.status)}</td> + <td className="p-2">{item.discovery_depth}</td> + <td className="p-2">{item.attempts}</td> + <td + className="p-2 max-w-xs truncate text-red-600" + title={item.error_message} + > + {item.error_message || "-"} + </td> + </tr> + ))} + </tbody> + </table> + </div> + </CardContent> + </Card> + + {/* Recent Sessions */} + <Card> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4">Recent Sessions</h3> + <div className="overflow-x-auto"> + <table className="w-full text-sm"> + <thead> + <tr className="border-b"> + <th className="text-left p-2">ID</th> + <th className="text-left p-2">Status</th> + <th className="text-left p-2">Discovered</th> + <th className="text-left p-2">Processed</th> + <th className="text-left p-2">Started</th> + <th className="text-left p-2">Error</th> + </tr> + </thead> + <tbody> + {data.sessions.map((session) => ( + <tr key={session.id} className="border-b"> + <td className="p-2 font-mono text-xs"> + {session.id.slice(0, 8)}... + </td> + <td className="p-2"> + {getStatusBadge(session.status)} + </td> + <td className="p-2">{session.pages_discovered}</td> + <td className="p-2">{session.pages_processed}</td> + <td className="p-2"> + {new Date(session.started_at).toLocaleString()} + </td> + <td + className="p-2 max-w-xs truncate text-red-600" + title={session.error_message} + > + {session.error_message || "-"} + </td> + </tr> + ))} + </tbody> + </table> + </div> + </CardContent> + </Card> + + {/* Recent Pages */} + <Card> + <CardContent className="p-6"> + <h3 className="text-lg font-semibold mb-4"> + Recent Discovered Pages + </h3> + <div className="overflow-x-auto"> + <table className="w-full text-sm"> + <thead> + <tr className="border-b"> + <th className="text-left p-2">URL</th> + <th className="text-left p-2">Title</th> + <th className="text-left p-2">Active</th> + <th className="text-left p-2">Depth</th> + <th className="text-left p-2">Created</th> + </tr> + </thead> + <tbody> + {data.pages.map((page) => ( + <tr key={page.id} className="border-b"> + <td className="p-2 max-w-xs truncate" title={page.url}> + {page.url} + </td> + <td + className="p-2 max-w-xs truncate" + title={page.title} + > + {page.title || "Untitled"} + </td> + <td className="p-2"> + <Badge + className={ + page.is_active + ? "bg-green-100 text-green-800" + : "bg-gray-100 text-gray-800" + } + > + {page.is_active ? "Active" : "Inactive"} + </Badge> + </td> + <td className="p-2">{page.depth}</td> + <td className="p-2"> + {new Date(page.created_at).toLocaleString()} + </td> + </tr> + ))} + </tbody> + </table> + </div> + </CardContent> + </Card> + </> + )} + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/demo/FakeCompetitorDashboard.tsx b/website-monitoring-frontend/src/components/demo/FakeCompetitorDashboard.tsx new file mode 100644 index 0000000..65b195b --- /dev/null +++ b/website-monitoring-frontend/src/components/demo/FakeCompetitorDashboard.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { CompetitorDashboardBase } from "@/components/core/competitorDashboard/CompetitorDashboardBase"; +import { LoadingSpinner } from "@/components/ui/feedback/LoadingSpinner"; +import { + generateCompetitorData, + updateCompetitorMetrics, +} from "@/utils/dataGenerators"; +import type { CompetitorData } from "@/types/metrics"; + +export const FakeCompetitorDashboard = () => { + const [isClient, setIsClient] = useState(false); + const [metrics, setMetrics] = useState<CompetitorData>( + generateCompetitorData(), + ); + const [isLoading, setIsLoading] = useState(true); + const intervalRef = useRef<NodeJS.Timeout | null>(null); + + useEffect(() => { + setIsClient(true); + setIsLoading(false); + }, []); + + useEffect(() => { + if (!isClient) return; + + setMetrics(generateCompetitorData()); + + intervalRef.current = setInterval(() => { + setMetrics((prevMetrics) => updateCompetitorMetrics(prevMetrics)); + }, 7000); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [isClient]); + + if (isLoading) { + return <LoadingSpinner />; + } + + return ( + <div className="w-full"> + <CompetitorDashboardBase data={metrics} isDemo={true} /> + </div> + ); +}; diff --git a/website-monitoring-frontend/src/components/demo/FakeDashboard.tsx b/website-monitoring-frontend/src/components/demo/FakeDashboard.tsx new file mode 100644 index 0000000..009a0fb --- /dev/null +++ b/website-monitoring-frontend/src/components/demo/FakeDashboard.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { DashboardBase } from "@/components/core/dashboard/DashboardBase"; +import { dataGenerators } from "@/utils/dataGenerators"; +import type { DashboardMetrics } from "@/types/metrics"; +import { LoadingSpinner } from "@/components/ui/feedback/LoadingSpinner"; + +export const FakeDashboard = () => { + const [isClient, setIsClient] = useState(false); + const [metrics, setMetrics] = useState<DashboardMetrics>( + dataGenerators.dashboard.generate(), + ); + const [isLoading, setIsLoading] = useState(true); + const intervalRef = useRef<NodeJS.Timeout | null>(null); + + useEffect(() => { + setIsClient(true); + setIsLoading(false); + }, []); + + useEffect(() => { + if (!isClient) return; + + setMetrics(dataGenerators.dashboard.generate()); + + intervalRef.current = setInterval(() => { + setMetrics((prevMetrics) => dataGenerators.dashboard.update(prevMetrics)); + }, 5000); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [isClient]); + + if (isLoading) { + return <LoadingSpinner />; + } + + return ( + <div className="w-full"> + <DashboardBase data={metrics} isDemo={true} /> + </div> + ); +}; diff --git a/website-monitoring-frontend/src/components/demo/HeroPage.tsx b/website-monitoring-frontend/src/components/demo/HeroPage.tsx new file mode 100644 index 0000000..3542a23 --- /dev/null +++ b/website-monitoring-frontend/src/components/demo/HeroPage.tsx @@ -0,0 +1,360 @@ +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; +import { motion, AnimatePresence, useAnimation } from "framer-motion"; +import { useInView } from "react-intersection-observer"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/layout/Tabs"; +import { Button } from "@/components/ui/forms/Button"; +import { FakeDashboard } from "@/components/demo/FakeDashboard"; +import { FakeCompetitorDashboard } from "@/components/demo/FakeCompetitorDashboard"; +import { ErrorBoundary } from "@/components/ui/feedback/ErrorBoundary"; +import { + BarChart3, + LineChart, + ArrowRight, + Globe, + Shield, + Zap, + Rocket, + Lightbulb, + Target, +} from "lucide-react"; +import { useRouter } from "next/navigation"; + +// Animation variants +const fadeInUp = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.6, + }, + }, +}; + +const staggerContainer = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.2, + }, + }, +}; + +export default function HeroPage() { + const [activeTab, setActiveTab] = useState("performance"); + const controls = useAnimation(); + const [ref, inView] = useInView({ + threshold: 0.1, + triggerOnce: true, + }); + + const router = useRouter(); + + useEffect(() => { + router.prefetch("/auth"); + }, [router]); + + // Features data + const features = useMemo( + () => [ + { + icon: <Zap className="h-8 w-8 text-blue-600" />, + title: "Real-time Monitoring", + description: + "Get instant insights on your website's performance with continuous monitoring and alerts.", + }, + { + icon: <Globe className="h-8 w-8 text-blue-600" />, + title: "Global Coverage", + description: + "Monitor your website's performance from multiple locations worldwide for comprehensive insights.", + }, + { + icon: <Rocket className="h-8 w-8 text-blue-600" />, + title: "Performance Optimization", + description: + "Get actionable recommendations to improve your website's speed and user experience.", + }, + { + icon: <Target className="h-8 w-8 text-blue-600" />, + title: "Competitor Analysis", + description: + "Benchmark your performance against competitors and industry standards.", + }, + { + icon: <Lightbulb className="h-8 w-8 text-blue-600" />, + title: "Smart Insights", + description: + "AI-powered analysis to identify optimization opportunities and trends.", + }, + { + icon: <Shield className="h-8 w-8 text-blue-600" />, + title: "Automated Audits", + description: + "Regular automated audits to ensure your website maintains optimal performance.", + }, + ], + [], + ); + + const handleTabChange = useCallback((value: string) => { + setActiveTab(value); + }, []); + + const handleCtaClick = useCallback(() => { + router.push("/auth?source=hero"); + }, [router]); + + + useEffect(() => { + if (inView) { + controls.start("visible"); + } + }, [controls, inView]); + + return ( + <ErrorBoundary> + <motion.main + className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100" + initial="hidden" + animate="visible" + variants={staggerContainer} + > + + {/* Navigation */} + <motion.nav + role="navigation" + aria-label="Main navigation" + className="bg-white shadow-sm py-4 sticky top-0 z-50" + initial={{ y: -100 }} + animate={{ y: 0 }} + transition={{ duration: 0.5 }} + > + <div className="max-w-7xl mx-auto px-4 sm:px-6 flex justify-between items-center"> + <motion.div + className="flex items-center" + whileHover={{ scale: 1.05 }} + transition={{ type: "spring", stiffness: 400, damping: 10 }} + > + <Shield className="h-8 w-8 text-blue-600 mr-2" /> + <span className="font-bold text-xl text-slate-800"> + CloudLense + </span> + </motion.div> + <div className="hidden md:flex items-center space-x-4"> + {["Features", "Pricing", "Documentation"].map((item, i) => ( + <motion.div + key={i} + whileHover={{ scale: 1.05 }} + transition={{ type: "spring", stiffness: 400, damping: 17 }} + > + <Button variant="ghost">{item}</Button> + </motion.div> + ))} + <motion.div + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + transition={{ type: "spring", stiffness: 400, damping: 17 }} + > + <Button onClick={handleCtaClick} variant="default"> + Sign Up/Log In + </Button> + </motion.div> + </div> + </div> + </motion.nav> + + {/* Hero Section */} + <section className="pt-12 md:pt-20 pb-16 md:pb-24 px-4 relative overflow-hidden"> + {/* Background animations */} + <motion.div + className="absolute top-20 left-10 w-72 h-72 bg-blue-500/10 rounded-full filter blur-3xl" + animate={{ + scale: [1, 1.2, 1], + opacity: [0.5, 0.7, 0.5], + }} + transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }} + /> + <motion.div + className="absolute bottom-20 right-10 w-96 h-96 bg-indigo-500/10 rounded-full filter blur-3xl" + animate={{ + scale: [1, 1.3, 1], + opacity: [0.5, 0.8, 0.5], + }} + transition={{ + duration: 10, + repeat: Infinity, + ease: "easeInOut", + delay: 1, + }} + /> + + {/* Hero Content */} + <div className="max-w-7xl mx-auto text-center mb-8 md:mb-16 relative z-10"> + <motion.div variants={fadeInUp}> + <motion.span + className="bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1 rounded-full inline-block" + whileHover={{ scale: 1.05 }} + > + Website Monitoring Made Simple + </motion.span> + <h1 className="text-4xl md:text-6xl font-bold mt-6 mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-indigo-600 leading-tight"> + Monitor Your Website's Performance + <br /> + <motion.span + className="inline-block" + animate={{ + color: ["#3b82f6", "#6366f1", "#3b82f6"], + }} + transition={{ duration: 4, repeat: Infinity }} + > + Like Never Before + </motion.span> + </h1> + <p className="text-lg md:text-xl text-gray-600 max-w-3xl mx-auto mb-8"> + Get comprehensive insights into your website's performance, + SEO, accessibility, and how you stack up against competitors - + all in one powerful, easy-to-use platform. + </p> + <div className="flex flex-col sm:flex-row justify-center gap-4 sm:space-x-4 mb-8 sm:mb-16"> + <Button + size="lg" + className="px-8 bg-gradient-to-r from-blue-600 to-indigo-600 hover:scale-110 hover:cursor-grab transition" + onClick={handleCtaClick} + > + Start Monitoring <ArrowRight className="ml-2 h-4 w-4" /> + </Button> + </div> + </motion.div> + </div> + + {/* Dashboard Preview */} + <div className="max-w-7xl mx-auto"> + <div className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-100"> + <Tabs + defaultValue="performance" + value={activeTab} + onValueChange={handleTabChange} + className="w-full" + > + {/* Tab Header */} + <div className="px-6 pt-6 border-b border-gray-200 bg-gray-50"> + <div className="flex justify-between items-center mb-4"> + <h2 className="text-2xl font-bold text-slate-800"> + Dashboard Preview + </h2> + <div className="flex items-center space-x-2"> + <motion.span + whileHover={{ scale: 1.2 }} + className="h-3 w-3 bg-red-500 rounded-full" + /> + <motion.span + whileHover={{ scale: 1.2 }} + className="h-3 w-3 bg-yellow-500 rounded-full" + /> + <motion.span + whileHover={{ scale: 1.2 }} + className="h-3 w-3 bg-green-500 rounded-full" + /> + </div> + </div> + <TabsList className="grid grid-cols-2 gap-4"> + <TabsTrigger + value="performance" + className="flex items-center justify-center" + > + <LineChart className="w-4 h-4 mr-2" /> + Performance Dashboard + </TabsTrigger> + <TabsTrigger + value="competitors" + className="flex items-center justify-center" + > + <BarChart3 className="w-4 h-4 mr-2" /> + Competitor Analysis + </TabsTrigger> + </TabsList> + </div> + + {/* Tab Content */} + <div className="overflow-hidden"> + <AnimatePresence mode="wait"> + {activeTab === "performance" && ( + <TabsContent value="performance" className="mt-0"> + <motion.div + key="performance" + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -20 }} + transition={{ duration: 0.3 }} + > + <FakeDashboard /> + </motion.div> + </TabsContent> + )} + {activeTab === "competitors" && ( + <TabsContent value="competitors" className="mt-0"> + <motion.div + key="competitors" + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -20 }} + transition={{ duration: 0.3 }} + > + <FakeCompetitorDashboard /> + </motion.div> + </TabsContent> + )} + </AnimatePresence> + </div> + </Tabs> + </div> + </div> + </section> + + {/* Features Section */} + <section + ref={ref} + aria-labelledby="features-heading" + className="py-20 bg-white" + > + <div className="max-w-7xl mx-auto px-4"> + <motion.h2 + id="features-heading" + className="text-3xl font-bold text-center mb-12" + variants={fadeInUp} + > + Key Features + </motion.h2> + <motion.div + className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" + variants={staggerContainer} + > + {features.map((feature, index) => ( + <motion.div + key={index} + variants={fadeInUp} + className="p-6 bg-gray-50 rounded-xl shadow-sm hover:shadow-md transition-all duration-300" + > + <div className="mb-4">{feature.icon}</div> + <h3 className="text-xl font-semibold mb-2"> + {feature.title} + </h3> + <p className="text-gray-600">{feature.description}</p> + </motion.div> + ))} + </motion.div> + </div> + </section> + </motion.main> + </ErrorBoundary> + ); +} diff --git a/website-monitoring-frontend/src/components/layouts/DashboardLayout.tsx b/website-monitoring-frontend/src/components/layouts/DashboardLayout.tsx new file mode 100644 index 0000000..53461e1 --- /dev/null +++ b/website-monitoring-frontend/src/components/layouts/DashboardLayout.tsx @@ -0,0 +1,21 @@ +import { Header } from "../shared/Header"; +import { Sidebar } from "../shared/Sidebar"; +import { ErrorBoundary } from "../ui/ErrorBoundary"; + +export function DashboardLayout({ children }: { children: React.ReactNode }) { + return ( + <div className="min-h-screen"> + <Header /> + <div className="flex pt-16"> + <Sidebar /> + <main className="flex-1 p-6 lg:ml-64"> + <div className="max-w-7xl mx-auto"> + <ErrorBoundary> + {children} + </ErrorBoundary> + </div> + </main> + </div> + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/monitoring/AlertsList.tsx b/website-monitoring-frontend/src/components/monitoring/AlertsList.tsx new file mode 100644 index 0000000..d779e1f --- /dev/null +++ b/website-monitoring-frontend/src/components/monitoring/AlertsList.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Alert } from "@/types/monitoring"; +import { AlertCircle, ChevronDown, ChevronUp } from "lucide-react"; + +interface AlertsListProps { + alerts: Alert[]; +} + +export function AlertsList({ alerts }: AlertsListProps) { + const [expandedAlertId, setExpandedAlertId] = useState<string | null>(null); + + const getSeverityColor = (severity: string) => { + switch (severity) { + case "high": + return "bg-red-100 text-red-800 border-red-200"; + case "medium": + return "bg-yellow-100 text-yellow-800 border-yellow-200"; + case "low": + return "bg-blue-100 text-blue-800 border-blue-200"; + default: + return "bg-gray-100 text-gray-800 border-gray-200"; + } + }; + + return ( + <div className="space-y-4"> + {alerts.map((alert) => ( + <motion.div + key={alert.id} + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: 10 }} + className="border rounded-lg overflow-hidden" + > + <div + className={`p-4 cursor-pointer ${getSeverityColor(alert.severity)}`} + onClick={() => + setExpandedAlertId(expandedAlertId === alert.id ? null : alert.id) + } + > + <div className="flex items-center justify-between"> + <div className="flex items-center space-x-3"> + <AlertCircle className="h-5 w-5" /> + <div> + <h4 className="font-medium">{alert.title}</h4> + <p className="text-sm opacity-75"> + {new Date(alert.created_at).toLocaleString()} + </p> + </div> + </div> + {expandedAlertId === alert.id ? ( + <ChevronUp className="h-5 w-5" /> + ) : ( + <ChevronDown className="h-5 w-5" /> + )} + </div> + </div> + + <AnimatePresence> + {expandedAlertId === alert.id && ( + <motion.div + initial={{ height: 0 }} + animate={{ height: "auto" }} + exit={{ height: 0 }} + transition={{ duration: 0.2 }} + className="overflow-hidden" + > + <div className="p-4 bg-white border-t"> + <p className="text-gray-700">{alert.message}</p> + <div className="mt-4 flex justify-end space-x-3"> + <button className="text-sm text-blue-600 hover:underline"> + Acknowledge + </button> + <button className="text-sm text-green-600 hover:underline"> + Mark as Resolved + </button> + </div> + </div> + </motion.div> + )} + </AnimatePresence> + </motion.div> + ))} + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/monitoring/MetricsDisplay.tsx b/website-monitoring-frontend/src/components/monitoring/MetricsDisplay.tsx new file mode 100644 index 0000000..aa911fb --- /dev/null +++ b/website-monitoring-frontend/src/components/monitoring/MetricsDisplay.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Card } from "@/components/ui/layout/Card"; +import { MetricValue } from "@/types/monitoring"; +import { TrendingUp, TrendingDown } from "lucide-react"; + +interface MetricsDisplayProps { + metrics: MetricValue[]; +} + +const METRIC_CONFIGS = { + performance: { + label: "Performance", + threshold: 90, + color: "blue", + }, + accessibility: { + label: "Accessibility", + threshold: 90, + color: "purple", + }, + seo: { + label: "SEO", + threshold: 90, + color: "green", + }, + bestPractices: { + label: "Best Practices", + threshold: 90, + color: "orange", + }, +}; + +export function MetricsDisplay({ metrics }: MetricsDisplayProps) { + const getLatestMetricValue = (key: string) => { + const relevantMetrics = metrics.filter((m) => m.metric_key === key); + return relevantMetrics[relevantMetrics.length - 1]?.value ?? 0; + }; + + const getTrend = (key: string) => { + const relevantMetrics = metrics.filter((m) => m.metric_key === key); + if (relevantMetrics.length < 2) return 0; + + const current = relevantMetrics[relevantMetrics.length - 1].value; + const previous = relevantMetrics[relevantMetrics.length - 2].value; + return current - previous; + }; + + return ( + <> + {Object.entries(METRIC_CONFIGS).map(([key, config]) => { + const value = getLatestMetricValue(key); + const trend = getTrend(key); + const isBelowThreshold = value < config.threshold; + + return ( + <Card key={key} className="p-4"> + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.3 }} + > + <div className="flex items-center justify-between mb-2"> + <h3 className="text-sm font-medium text-gray-500"> + {config.label} + </h3> + <div + className={`h-2 w-2 rounded-full ${ + isBelowThreshold ? "bg-red-500" : "bg-green-500" + }`} + /> + </div> + + <div className="flex items-end justify-between"> + <div className="text-2xl font-bold"> + {Math.round(value)} + <span className="text-sm font-normal text-gray-500 ml-1"> + /100 + </span> + </div> + + {trend !== 0 && ( + <motion.div + initial={{ opacity: 0, scale: 0.5 }} + animate={{ opacity: 1, scale: 1 }} + className={`flex items-center ${ + trend > 0 ? "text-green-500" : "text-red-500" + }`} + > + {trend > 0 ? ( + <TrendingUp className="h-4 w-4 mr-1" /> + ) : ( + <TrendingDown className="h-4 w-4 mr-1" /> + )} + <span className="text-sm"> + {Math.abs(trend).toFixed(1)}% + </span> + </motion.div> + )} + </div> + + <div className="mt-4 h-2 bg-gray-100 rounded-full overflow-hidden"> + <motion.div + className={`h-full bg-${config.color}-500`} + initial={{ width: 0 }} + animate={{ width: `${value}%` }} + transition={{ duration: 0.5 }} + /> + </div> + </motion.div> + </Card> + ); + })} + </> + ); +} diff --git a/website-monitoring-frontend/src/components/monitoring/MonitoringDashboard.tsx b/website-monitoring-frontend/src/components/monitoring/MonitoringDashboard.tsx new file mode 100644 index 0000000..3741937 --- /dev/null +++ b/website-monitoring-frontend/src/components/monitoring/MonitoringDashboard.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { useEffect } from "react"; +import { useMonitoring } from "@/contexts/MonitoringContext"; +import { MetricsDisplay } from "./MetricsDisplay"; +import { AlertsList } from "./AlertsList"; +import { PerformanceGraph } from "./PerformanceGraph"; +import { ResourceUsage } from "./ResourceUsage"; +import { Card } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { AlertCircle, Pause, Play } from "lucide-react"; + +interface MonitoringDashboardProps { + websiteId: string; +} + +export function MonitoringDashboard({ websiteId }: MonitoringDashboardProps) { + const { + latestScan, + alerts, + metrics, + isMonitoring, + startMonitoring, + stopMonitoring, + } = useMonitoring(); + + useEffect(() => { + startMonitoring(websiteId); + return () => stopMonitoring(); + }, [websiteId]); + + return ( + <div className="space-y-6"> + {/* Monitoring Controls */} + <div className="flex items-center justify-between"> + <h2 className="text-2xl font-bold">Live Monitoring</h2> + <Button + onClick={() => + isMonitoring ? stopMonitoring() : startMonitoring(websiteId) + } + variant={isMonitoring ? "outline" : "default"} + > + {isMonitoring ? ( + <> + <Pause className="w-4 h-4 mr-2" /> Pause Monitoring + </> + ) : ( + <> + <Play className="w-4 h-4 mr-2" /> Start Monitoring + </> + )} + </Button> + </div> + + {/* Current Status */} + {latestScan && ( + <Card className="p-4"> + <div className="flex items-center space-x-4"> + <div + className={`h-3 w-3 rounded-full ${ + latestScan.status === "completed" + ? "bg-green-500" + : latestScan.status === "running" + ? "bg-blue-500" + : "bg-yellow-500" + }`} + /> + <span className="font-medium"> + Status:{" "} + {latestScan.status.charAt(0).toUpperCase() + + latestScan.status.slice(1)} + </span> + <span className="text-gray-500"> + Last updated:{" "} + {new Date(latestScan.updated_at).toLocaleTimeString()} + </span> + </div> + </Card> + )} + + {/* Metrics Overview */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> + <MetricsDisplay metrics={metrics} /> + </div> + + {/* Performance Graph */} + <Card className="p-4"> + <PerformanceGraph data={metrics} /> + </Card> + + {/* Resource Usage */} + {latestScan?.id && ( + <Card className="p-4"> + <ResourceUsage scanId={latestScan.id} /> + </Card> + )} + + {/* Alerts */} + {alerts.length > 0 && ( + <Card className="p-4"> + <div className="flex items-center space-x-2 mb-4"> + <AlertCircle className="w-5 h-5 text-red-500" /> + <h3 className="text-lg font-semibold">Active Alerts</h3> + </div> + <AlertsList alerts={alerts} /> + </Card> + )} + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/monitoring/PerformanceGraph.tsx b/website-monitoring-frontend/src/components/monitoring/PerformanceGraph.tsx new file mode 100644 index 0000000..73e1749 --- /dev/null +++ b/website-monitoring-frontend/src/components/monitoring/PerformanceGraph.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useMemo, useState, useMemo as useReactMemo, useEffect } from "react"; +import "@/lib/chart"; +import { SafeLineChart } from "@/components/ui/data-display/SafeLineChart"; +import { MetricValue } from "@/types/monitoring"; + +interface PerformanceGraphProps { + data: MetricValue[]; +} + +export function PerformanceGraph({ data }: PerformanceGraphProps) { + const [timeRange, setTimeRange] = useState("1h"); + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + + const filteredData = useMemo(() => { + const now = new Date(); + const timeRangeInMs = { + "1h": 60 * 60 * 1000, + "24h": 24 * 60 * 60 * 1000, + "7d": 7 * 24 * 60 * 60 * 1000, + }[timeRange] || 60 * 60 * 1000; // Default to 1h if timeRange is invalid + + return data.filter( + (item) => + new Date(item.timestamp).getTime() > now.getTime() - timeRangeInMs, + ); + }, [data, timeRange]); + + const chartKey = useReactMemo( + () => `perf-${timeRange}-${filteredData.length}`, + [timeRange, filteredData.length], + ); + + const chartData = { + labels: filteredData.map((item) => + new Date(item.timestamp).toLocaleTimeString(), + ), + datasets: [ + { + id: "performance-score", + label: "Performance Score", + data: filteredData.map((item) => item.value), + borderColor: "rgb(59, 130, 246)", + backgroundColor: "rgba(59, 130, 246, 0.1)", + fill: true, + tension: 0.4, + }, + ], + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + tooltip: { + mode: "index" as const, + intersect: false, + }, + }, + scales: { + y: { + min: 0, + max: 100, + ticks: { + stepSize: 20, + }, + }, + }, + interaction: { + mode: "nearest" as const, + axis: "x" as const, + intersect: false, + }, + }; + + return ( + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <h3 className="text-lg font-medium">Performance Trend</h3> + <div className="flex space-x-2"> + {["1h", "24h", "7d"].map((range) => ( + <button + key={range} + onClick={() => setTimeRange(range)} + className={`px-3 py-1 rounded-md text-sm ${ + timeRange === range + ? "bg-blue-100 text-blue-700" + : "text-gray-600 hover:bg-gray-100" + }`} + > + {range} + </button> + ))} + </div> + </div> + + <div className="h-64"> + {mounted && chartData.labels.length > 0 && chartData.datasets[0].data.length > 0 && ( + <SafeLineChart key={chartKey} data={chartData} options={options} /> + )} + </div> + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/monitoring/ResourceUsage.tsx b/website-monitoring-frontend/src/components/monitoring/ResourceUsage.tsx new file mode 100644 index 0000000..7119b7c --- /dev/null +++ b/website-monitoring-frontend/src/components/monitoring/ResourceUsage.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { supabase } from "@/lib/supabase"; +import { ResourceUsageData } from "@/types/monitoring"; +import { motion } from "framer-motion"; +import { File, FileText, Image, Code } from "lucide-react"; + +interface ResourceUsageProps { + scanId: string; +} + +export function ResourceUsage({ scanId }: ResourceUsageProps) { + const [resources, setResources] = useState<ResourceUsageData[]>([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (scanId) { + loadResourceData(); + } + }, [scanId]); + + const loadResourceData = async () => { + try { + const { data, error } = await supabase + .from("resource_analysis") + .select("*") + .eq("scan_id", scanId); + + if (error) throw error; + setResources(data); + } catch (error) { + console.error("Failed to load resource data:", error); + } finally { + setLoading(false); + } + }; + + const getResourceIcon = (type: string) => { + switch (type) { + case "script": + return <Code className="h-5 w-5" />; + case "stylesheet": + return <FileText className="h-5 w-5" />; + case "image": + return <Image className="h-5 w-5" />; + default: + return <File className="h-5 w-5" />; + } + }; + + const formatSize = (bytes: number) => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + }; + + const getTotalSize = () => + resources.reduce((acc, resource) => acc + resource.size_bytes, 0); + + if (loading) return <div>Loading resource data...</div>; + + return ( + <div className="space-y-6"> + <div> + <h3 className="text-lg font-medium mb-2">Resource Usage</h3> + <p className="text-sm text-gray-600"> + Total Size: {formatSize(getTotalSize())} + </p> + </div> + + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + {Object.entries( + resources.reduce( + (acc, resource) => { + const type = resource.resource_type; + if (!acc[type]) acc[type] = { count: 0, size: 0 }; + acc[type].count++; + acc[type].size += resource.size_bytes; + return acc; + }, + {} as Record<string, { count: number; size: number }>, + ), + ).map(([type, data]) => ( + <motion.div + key={type} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + className="bg-gray-50 rounded-lg p-4" + > + <div className="flex items-center space-x-3"> + {getResourceIcon(type)} + <div> + <h4 className="font-medium capitalize">{type}</h4> + <p className="text-sm text-gray-600"> + {data.count} files • {formatSize(data.size)} + </p> + </div> + </div> + </motion.div> + ))} + </div> + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/shared/Header.tsx b/website-monitoring-frontend/src/components/shared/Header.tsx new file mode 100644 index 0000000..b0ce04f --- /dev/null +++ b/website-monitoring-frontend/src/components/shared/Header.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { useAuth } from "@/contexts/AuthContext"; +import { Button } from "@/components/ui/forms/Button"; +import { Shield, Menu, X } from "lucide-react"; +import { useState } from "react"; +import Link from "next/link"; + +export function Header() { + const { user, signOut } = useAuth(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + return ( + <header className="bg-white border-b border-gray-200 fixed top-0 inset-x-0 z-30"> + <nav className="mx-auto px-4 sm:px-6 lg:px-8" aria-label="Top"> + <div className="w-full py-4 flex items-center justify-between"> + <div className="flex items-center"> + <Link href="/" className="flex items-center"> + <Shield className="h-8 w-8 text-blue-600" /> + <span className="ml-2 text-xl font-bold text-gray-900"> + CloudLense + </span> + </Link> + </div> + + {/* Desktop navigation */} + <div className="hidden lg:flex lg:items-center lg:space-x-6"> + {user && ( + <> + <Link + href="/dashboard" + className="text-base font-medium text-gray-700 hover:text-gray-900" + > + Dashboard + </Link> + <Link + href="/dashboard/websites" + className="text-base font-medium text-gray-700 hover:text-gray-900" + > + Websites + </Link> + <Link + href="/dashboard/settings" + className="text-base font-medium text-gray-700 hover:text-gray-900" + > + Settings + </Link> + <Button onClick={() => signOut()} variant="outline"> + Sign Out + </Button> + </> + )} + </div> + + {/* Mobile menu button */} + <div className="lg:hidden"> + <Button + variant="ghost" + onClick={() => setMobileMenuOpen(!mobileMenuOpen)} + > + {mobileMenuOpen ? ( + <X className="h-6 w-6" /> + ) : ( + <Menu className="h-6 w-6" /> + )} + </Button> + </div> + </div> + + {/* Mobile menu */} + {mobileMenuOpen && ( + <div className="lg:hidden py-4"> + <div className="flex flex-col space-y-4"> + {user && ( + <> + <Link + href="/dashboard" + className="text-base font-medium text-gray-700 hover:text-gray-900" + onClick={() => setMobileMenuOpen(false)} + > + Dashboard + </Link> + <Link + href="/dashboard/websites" + className="text-base font-medium text-gray-700 hover:text-gray-900" + onClick={() => setMobileMenuOpen(false)} + > + Websites + </Link> + <Link + href="/dashboard/settings" + className="text-base font-medium text-gray-700 hover:text-gray-900" + onClick={() => setMobileMenuOpen(false)} + > + Settings + </Link> + <Button + onClick={() => { + signOut(); + setMobileMenuOpen(false); + }} + variant="outline" + className="w-full" + > + Sign Out + </Button> + </> + )} + </div> + </div> + )} + </nav> + </header> + ); +} diff --git a/website-monitoring-frontend/src/components/shared/Sidebar.tsx b/website-monitoring-frontend/src/components/shared/Sidebar.tsx new file mode 100644 index 0000000..b52cd96 --- /dev/null +++ b/website-monitoring-frontend/src/components/shared/Sidebar.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import Link from "next/link"; +import { motion } from "framer-motion"; +import { + BarChart2, + Globe, + Settings, + Bell, + Users, + Activity, + Zap, + Search, +} from "lucide-react"; + +interface SidebarItem { + name: string; + href: string; + icon: React.ElementType; +} + +const navigation: SidebarItem[] = [ + { name: "Overview", href: "/dashboard", icon: BarChart2 }, + { name: "Websites", href: "/dashboard/websites", icon: Globe }, + { name: "Performance", href: "/dashboard/performance", icon: Zap }, + { name: "SEO", href: "/dashboard/seo", icon: Search }, + { name: "Monitoring", href: "/dashboard/monitoring", icon: Activity }, + { name: "Alerts", href: "/dashboard/alerts", icon: Bell }, + { name: "Team", href: "/dashboard/team", icon: Users }, + { name: "Settings", href: "/dashboard/settings", icon: Settings }, +]; + +export function Sidebar() { + const pathname = usePathname(); + + return ( + <div className="hidden lg:block lg:fixed lg:inset-y-0 lg:z-10 lg:w-64 lg:flex-col"> + <div className="flex flex-col flex-grow overflow-y-auto bg-white pt-16"> + {" "} + {/* Added pt-16 to account for header */} + <div className="flex-grow flex flex-col"> + <nav className="flex-1 px-2 py-4 space-y-1"> + {navigation.map((item) => { + const isActive = pathname === item.href; + const Icon = item.icon; + + return ( + <Link + key={item.name} + href={item.href} + className={`relative group flex items-center px-2 py-2 text-sm font-medium rounded-md ${ + isActive + ? "text-blue-600" + : "text-gray-600 hover:text-blue-600" + }`} + > + {isActive && ( + <motion.div + layoutId="activeTab" + className="absolute inset-0 bg-blue-50 rounded-md" + initial={false} + transition={{ + type: "spring", + stiffness: 500, + damping: 30, + }} + /> + )} + <div className="relative flex items-center"> + <Icon + className={`mr-3 h-5 w-5 transition-colors ${ + isActive + ? "text-blue-600" + : "text-gray-400 group-hover:text-blue-600" + }`} + /> + {item.name} + </div> + </Link> + ); + })} + </nav> + </div> + </div> + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/ui/DatabaseSetupHelper.tsx b/website-monitoring-frontend/src/components/ui/DatabaseSetupHelper.tsx new file mode 100644 index 0000000..88412e7 --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/DatabaseSetupHelper.tsx @@ -0,0 +1,347 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { supabase } from "@/lib/supabase"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/layout/Card"; +import { Badge } from "@/components/ui/layout/Badge"; +import { Button } from "@/components/ui/forms/Button"; +import { CheckCircle, XCircle, AlertTriangle, Copy, Download } from "lucide-react"; +import { logError } from "@/utils/errorUtils"; + +interface TableCheck { + name: string; + exists: boolean; + required: boolean; + description: string; +} + +export function DatabaseSetupHelper() { + const [tableChecks, setTableChecks] = useState<TableCheck[]>([]); + const [loading, setLoading] = useState(false); + const [copied, setCopied] = useState(false); + + const requiredTables = [ + { name: "organizations", required: true, description: "Organization management" }, + { name: "users", required: true, description: "User accounts and roles" }, + { name: "websites", required: true, description: "Website configurations" }, + { name: "pages", required: true, description: "Website pages (MISSING - causing 400 errors)" }, + { name: "scans", required: true, description: "Scan records (MISSING - causing 400 errors)" }, + { name: "scan_results", required: true, description: "Scan results (MISSING - causing 400 errors)" }, + { name: "metric_values", required: true, description: "Performance metrics" }, + { name: "alerts", required: true, description: "Alert system (MISSING - causing 400 errors)" }, + { name: "alert_configurations", required: true, description: "Alert rules" }, + { name: "crawl_queue", required: false, description: "Crawl management" }, + { name: "crawl_sessions", required: false, description: "Crawl sessions" }, + { name: "metric_definitions", required: true, description: "Metric definitions" }, + { name: "resource_analysis", required: false, description: "Resource analysis" }, + ]; + + const checkTables = async () => { + setLoading(true); + const checks: TableCheck[] = []; + + for (const table of requiredTables) { + try { + const { error } = await supabase + .from(table.name) + .select("id") + .limit(1); + + checks.push({ + name: table.name, + exists: !error, + required: table.required, + description: table.description, + }); + } catch (error) { + logError(`Error checking table ${table.name}`, error); + checks.push({ + name: table.name, + exists: false, + required: table.required, + description: table.description, + }); + } + } + + setTableChecks(checks); + setLoading(false); + }; + + const copySchemaSQL = () => { + const sqlContent = `-- Copy and paste this into your Supabase SQL editor +-- This will create all missing tables + +-- Enable extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +CREATE EXTENSION IF NOT EXISTS "pg_stat_statements"; +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +-- Create missing tables +CREATE TABLE IF NOT EXISTS pages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + url VARCHAR NOT NULL, + path VARCHAR NOT NULL, + title VARCHAR, + description TEXT, + content_hash VARCHAR, + content_type VARCHAR, + status_code INTEGER, + is_active BOOLEAN DEFAULT true, + priority INTEGER DEFAULT 1, + depth INTEGER DEFAULT 0, + parent_page_id UUID REFERENCES pages(id), + discovery_method VARCHAR DEFAULT 'crawl', + last_seen_at TIMESTAMPTZ, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(website_id, url) +); + +CREATE TABLE IF NOT EXISTS scans ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + page_id UUID REFERENCES pages(id) NOT NULL, + triggered_by UUID REFERENCES users(id), + scan_type VARCHAR NOT NULL DEFAULT 'full', + status VARCHAR DEFAULT 'pending', + priority INTEGER DEFAULT 1, + categories VARCHAR[] DEFAULT ARRAY['performance', 'seo', 'accessibility', 'best_practices'], + device_type VARCHAR DEFAULT 'desktop', + user_agent VARCHAR, + lighthouse_version VARCHAR, + chrome_version VARCHAR, + environment JSONB DEFAULT '{}'::jsonb, + started_at TIMESTAMPTZ DEFAULT NOW(), + completed_at TIMESTAMPTZ, + duration_ms INTEGER, + error_message TEXT, + retry_count INTEGER DEFAULT 0, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS scan_results ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + scan_id UUID REFERENCES scans(id) NOT NULL, + category VARCHAR NOT NULL, + score NUMERIC, + raw_data JSONB, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS alerts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + website_id UUID REFERENCES websites(id) NOT NULL, + page_id UUID REFERENCES pages(id), + config_id UUID, + metric_id UUID, + severity VARCHAR DEFAULT 'medium', + title VARCHAR NOT NULL, + message TEXT NOT NULL, + details JSONB DEFAULT '{}'::jsonb, + status VARCHAR DEFAULT 'open', + acknowledged_by UUID REFERENCES users(id), + acknowledged_at TIMESTAMPTZ, + resolved_at TIMESTAMPTZ, + resolution_note TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Create indexes +CREATE INDEX IF NOT EXISTS idx_pages_website_active ON pages(website_id, is_active); +CREATE INDEX IF NOT EXISTS idx_scans_website_status ON scans(website_id, status); +CREATE INDEX IF NOT EXISTS idx_scan_results_scan_id ON scan_results(scan_id); +CREATE INDEX IF NOT EXISTS idx_alerts_website_status ON alerts(website_id, status); + +-- Enable RLS +ALTER TABLE pages ENABLE ROW LEVEL SECURITY; +ALTER TABLE scans ENABLE ROW LEVEL SECURITY; +ALTER TABLE scan_results ENABLE ROW LEVEL SECURITY; +ALTER TABLE alerts ENABLE ROW LEVEL SECURITY; + +-- Basic RLS policies +CREATE POLICY "Enable read access for authenticated users" ON pages FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON scans FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON scan_results FOR SELECT USING (true); +CREATE POLICY "Enable read access for authenticated users" ON alerts FOR SELECT USING (true); + +-- Insert sample data +INSERT INTO pages (id, website_id, url, path, title, is_active) +SELECT + '00000000-0000-0000-0000-000000000003', + w.id, + w.base_url, + '/', + w.name || ' Homepage', + true +FROM websites w +LIMIT 1 +ON CONFLICT (id) DO NOTHING; + +INSERT INTO scans (id, website_id, page_id, status, scan_type, device_type) +SELECT + '00000000-0000-0000-0000-000000000004', + w.id, + p.id, + 'completed', + 'full', + 'desktop' +FROM websites w +JOIN pages p ON p.website_id = w.id +LIMIT 1 +ON CONFLICT (id) DO NOTHING; + +INSERT INTO scan_results (scan_id, category, score, raw_data) +VALUES + ('00000000-0000-0000-0000-000000000004', 'performance', 85, '{"firstContentfulPaint": 1200}'), + ('00000000-0000-0000-0000-000000000004', 'seo', 92, '{"metaDescription": true}'), + ('00000000-0000-0000-0000-000000000004', 'accessibility', 88, '{"ariaLabels": 5}'), + ('00000000-0000-0000-0000-000000000004', 'best_practices', 95, '{"usesHttps": true}') +ON CONFLICT DO NOTHING;`; + + navigator.clipboard.writeText(sqlContent); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const downloadSetupScript = () => { + const element = document.createElement("a"); + const file = new Blob([`-- Website Monitoring Database Setup +-- Run this in your Supabase SQL editor + +-- This script creates all missing tables that are causing 400 errors +-- Copy the contents of setup-database.sql file and run it in Supabase + +-- After running this script, your dashboard pages should work correctly +-- The 400 errors for scans, websites, and alerts should be resolved + +-- For the complete setup script, see the setup-database.sql file in your project root`], {type: 'text/plain'}); + element.href = URL.createObjectURL(file); + element.download = "database-setup-instructions.txt"; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + }; + + useEffect(() => { + checkTables(); + }, []); + + const missingRequiredTables = tableChecks.filter(check => check.required && !check.exists); + const allRequiredTablesExist = missingRequiredTables.length === 0; + + return ( + <Card className="w-full max-w-4xl mx-auto"> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <AlertTriangle className="h-5 w-5 text-yellow-500" /> + Database Setup Helper + </CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div className="flex items-center justify-between"> + <div> + <p className="text-sm text-gray-600 mb-2"> + This helper checks if all required database tables exist. Missing tables are causing the 400 errors you're seeing. + </p> + {missingRequiredTables.length > 0 && ( + <div className="bg-red-50 border border-red-200 rounded-md p-3"> + <p className="text-sm text-red-800 font-medium"> + ⚠️ {missingRequiredTables.length} required table(s) are missing. This is causing the 400 errors. + </p> + </div> + )} + </div> + <div className="flex gap-2"> + <Button + onClick={checkTables} + disabled={loading} + variant="outline" + size="sm" + > + {loading ? "Checking..." : "Refresh"} + </Button> + <Button + onClick={copySchemaSQL} + variant="outline" + size="sm" + className="flex items-center gap-1" + > + <Copy className="h-4 w-4" /> + {copied ? "Copied!" : "Copy SQL"} + </Button> + <Button + onClick={downloadSetupScript} + variant="outline" + size="sm" + className="flex items-center gap-1" + > + <Download className="h-4 w-4" /> + Instructions + </Button> + </div> + </div> + + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> + {tableChecks.map((check) => ( + <div + key={check.name} + className={`p-3 rounded-md border ${ + check.exists + ? "bg-green-50 border-green-200" + : check.required + ? "bg-red-50 border-red-200" + : "bg-gray-50 border-gray-200" + }`} + > + <div className="flex items-center justify-between mb-1"> + <span className="font-mono text-sm font-medium">{check.name}</span> + <Badge + variant={check.exists ? "default" : check.required ? "destructive" : "secondary"} + className="text-xs" + > + {check.exists ? "Exists" : check.required ? "Missing" : "Optional"} + </Badge> + </div> + <p className="text-xs text-gray-600">{check.description}</p> + </div> + ))} + </div> + + {!allRequiredTablesExist && ( + <div className="bg-yellow-50 border border-yellow-200 rounded-md p-4"> + <h3 className="font-medium text-yellow-800 mb-2">How to Fix the 400 Errors:</h3> + <ol className="text-sm text-yellow-700 space-y-1 list-decimal list-inside"> + <li>Go to your Supabase project dashboard</li> + <li>Navigate to the SQL Editor</li> + <li>Copy the SQL from the "Copy SQL" button above</li> + <li>Paste and run the SQL in the Supabase SQL Editor</li> + <li>Refresh this page to verify the tables were created</li> + </ol> + <p className="text-xs text-yellow-600 mt-2"> + Alternatively, you can run the complete setup script from the <code>setup-database.sql</code> file in your project root. + </p> + </div> + )} + + {allRequiredTablesExist && ( + <div className="bg-green-50 border border-green-200 rounded-md p-4"> + <div className="flex items-center gap-2"> + <CheckCircle className="h-5 w-5 text-green-500" /> + <span className="font-medium text-green-800">All required tables exist!</span> + </div> + <p className="text-sm text-green-700 mt-1"> + Your database is properly set up. The 400 errors should now be resolved. + </p> + </div> + )} + </CardContent> + </Card> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/components/ui/ErrorBoundary.tsx b/website-monitoring-frontend/src/components/ui/ErrorBoundary.tsx new file mode 100644 index 0000000..c37365b --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/ErrorBoundary.tsx @@ -0,0 +1,72 @@ +"use client"; + +import React from "react"; +import { AlertTriangle, RefreshCw } from "lucide-react"; +import { Button } from "@/components/ui/forms/Button"; + +interface ErrorBoundaryState { + hasError: boolean; + error?: Error; +} + +interface ErrorBoundaryProps { + children: React.ReactNode; + fallback?: React.ComponentType<{ error?: Error; retry: () => void }>; +} + +export class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("Error caught by boundary:", error, errorInfo); + } + + retry = () => { + this.setState({ hasError: false, error: undefined }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + const FallbackComponent = this.props.fallback; + return <FallbackComponent error={this.state.error} retry={this.retry} />; + } + + return ( + <div className="flex flex-col items-center justify-center p-8 text-center"> + <AlertTriangle className="w-12 h-12 text-red-500 mb-4" /> + <h2 className="text-xl font-semibold text-gray-900 mb-2"> + Something went wrong + </h2> + <p className="text-gray-600 mb-6 max-w-md"> + An error occurred while loading this section. Please try refreshing. + </p> + <Button onClick={this.retry} className="flex items-center gap-2"> + <RefreshCw className="w-4 h-4" /> + Try Again + </Button> + </div> + ); + } + + return this.props.children; + } +} + +// Hook for functional components to handle errors +export function useErrorHandler() { + return (error: Error, errorInfo?: React.ErrorInfo) => { + console.error("Error handled:", error, errorInfo); + // You could also send to an error reporting service here + }; +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/components/ui/ErrorFeedback.tsx b/website-monitoring-frontend/src/components/ui/ErrorFeedback.tsx new file mode 100644 index 0000000..94a6eb8 --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/ErrorFeedback.tsx @@ -0,0 +1,85 @@ +import { motion } from "framer-motion"; +import { AlertCircle, AlertTriangle, XCircle, RefreshCw } from "lucide-react"; +import { Button } from "@/components/ui/forms/Button"; + +export type ErrorSeverity = "info" | "warning" | "error"; +export type ErrorAction = { + label: string; + onClick: () => void; + icon?: React.ReactNode; +}; + +interface ErrorFeedbackProps { + title?: string; + message: string; + details?: string | null; + severity?: ErrorSeverity; + actions?: ErrorAction[]; + onDismiss?: () => void; +} + +export function ErrorFeedback({ + title, + message, + details, + severity = "error", + actions = [], + onDismiss, +}: ErrorFeedbackProps) { + const colorMap = { + info: "bg-blue-50 text-blue-600 border-blue-100", + warning: "bg-amber-50 text-amber-600 border-amber-100", + error: "bg-red-50 text-red-600 border-red-100", + }; + + const iconMap = { + info: <AlertCircle className="h-5 w-5" />, + warning: <AlertTriangle className="h-5 w-5" />, + error: <XCircle className="h-5 w-5" />, + }; + + return ( + <motion.div + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + className={`p-3 rounded-lg border ${colorMap[severity]}`} + > + <div className="flex gap-3"> + <div className="mt-0.5 flex-shrink-0">{iconMap[severity]}</div> + <div className="flex-1"> + {title && <p className="font-medium">{title}</p>} + <p className={title ? "text-sm" : ""}>{message}</p> + {details && <p className="mt-1 text-sm opacity-80">{details}</p>} + + {(actions.length > 0 || onDismiss) && ( + <div className="mt-2 flex flex-wrap gap-2"> + {actions.map((action, index) => ( + <Button + key={index} + size="sm" + variant="outline" + onClick={action.onClick} + className="px-3 py-1 h-auto text-sm" + > + {action.icon && <span className="mr-1">{action.icon}</span>} + {action.label} + </Button> + ))} + {onDismiss && ( + <Button + size="sm" + variant="ghost" + onClick={onDismiss} + className="px-3 py-1 h-auto text-sm" + > + Dismiss + </Button> + )} + </div> + )} + </div> + </div> + </motion.div> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/components/ui/SupabaseDiagnostic.tsx b/website-monitoring-frontend/src/components/ui/SupabaseDiagnostic.tsx new file mode 100644 index 0000000..06d2615 --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/SupabaseDiagnostic.tsx @@ -0,0 +1,286 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/layout/Card"; +import { Button } from "@/components/ui/forms/Button"; +import { Badge } from "@/components/ui/layout/Badge"; +import { + Database, + CheckCircle, + XCircle, + AlertTriangle, + Loader2, +} from "lucide-react"; +import { supabase } from "@/lib/supabase"; +import { useAuth } from "@/contexts/AuthContext"; + +interface DiagnosticCheck { + name: string; + status: "checking" | "success" | "error" | "warning"; + message: string; + details?: string; +} + +export function SupabaseDiagnostic() { + const { user, userDetails } = useAuth(); + const [checks, setChecks] = useState<DiagnosticCheck[]>([]); + const [running, setRunning] = useState(false); + + const runDiagnostics = async () => { + setRunning(true); + const diagnostics: DiagnosticCheck[] = []; + + // Check 1: Environment Variables + diagnostics.push({ + name: "Environment Variables", + status: "checking", + message: "Checking Supabase configuration...", + }); + + const hasUrl = !!process.env.NEXT_PUBLIC_SUPABASE_URL; + const hasKey = !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + diagnostics[0] = { + name: "Environment Variables", + status: hasUrl && hasKey ? "success" : "error", + message: hasUrl && hasKey ? "Environment variables configured" : "Missing environment variables", + details: `URL: ${hasUrl ? "✓" : "✗"}, Key: ${hasKey ? "✓" : "✗"}`, + }; + + setChecks([...diagnostics]); + + // Check 2: Supabase Connection + diagnostics.push({ + name: "Supabase Connection", + status: "checking", + message: "Testing connection to Supabase...", + }); + + try { + const { data, error } = await supabase.auth.getSession(); + diagnostics[1] = { + name: "Supabase Connection", + status: error ? "error" : "success", + message: error ? "Connection failed" : "Successfully connected to Supabase", + details: error?.message, + }; + } catch (error) { + diagnostics[1] = { + name: "Supabase Connection", + status: "error", + message: "Connection test failed", + details: error instanceof Error ? error.message : "Unknown error", + }; + } + + setChecks([...diagnostics]); + + // Check 3: Authentication Status + diagnostics.push({ + name: "Authentication", + status: "checking", + message: "Checking authentication status...", + }); + + diagnostics[2] = { + name: "Authentication", + status: user ? "success" : "warning", + message: user ? "User authenticated" : "No user session", + details: user ? `User ID: ${user.id}` : "Please log in to test database access", + }; + + setChecks([...diagnostics]); + + // Check 4: User Data Access (only if authenticated) + if (user) { + diagnostics.push({ + name: "User Data Access", + status: "checking", + message: "Testing user data access...", + }); + + try { + const { data: userData, error } = await supabase + .from("users") + .select("id, name, organization_id") + .eq("id", user.id) + .single(); + + diagnostics[3] = { + name: "User Data Access", + status: error ? "error" : "success", + message: error ? "Cannot access user data" : "User data accessible", + details: error ? error.message : `Organization ID: ${userData?.organization_id || "None"}`, + }; + } catch (error) { + diagnostics[3] = { + name: "User Data Access", + status: "error", + message: "User data query failed", + details: error instanceof Error ? error.message : "Unknown error", + }; + } + + setChecks([...diagnostics]); + + // Check 5: Organizations Table Access + if (userDetails?.organization_id) { + diagnostics.push({ + name: "Organization Access", + status: "checking", + message: "Testing organization data access...", + }); + + try { + const { data: orgData, error } = await supabase + .from("organizations") + .select("id, name") + .eq("id", userDetails.organization_id) + .single(); + + diagnostics[4] = { + name: "Organization Access", + status: error ? "error" : "success", + message: error ? "Cannot access organization data" : "Organization data accessible", + details: error ? error.message : `Organization: ${orgData?.name || "Unknown"}`, + }; + } catch (error) { + diagnostics[4] = { + name: "Organization Access", + status: "error", + message: "Organization query failed", + details: error instanceof Error ? error.message : "Unknown error", + }; + } + + setChecks([...diagnostics]); + + // Check 6: Websites Table Access + diagnostics.push({ + name: "Websites Table Access", + status: "checking", + message: "Testing websites table access...", + }); + + try { + const { data: websites, error } = await supabase + .from("websites") + .select("id") + .eq("organization_id", userDetails.organization_id) + .limit(1); + + diagnostics[5] = { + name: "Websites Table Access", + status: error ? "error" : "success", + message: error ? "Cannot access websites table" : "Websites table accessible", + details: error ? error.message : `Query successful`, + }; + } catch (error) { + diagnostics[5] = { + name: "Websites Table Access", + status: "error", + message: "Websites table query failed", + details: error instanceof Error ? error.message : "Unknown error", + }; + } + + setChecks([...diagnostics]); + } + } + + setRunning(false); + }; + + useEffect(() => { + runDiagnostics(); + }, [user, userDetails]); + + const getStatusIcon = (status: DiagnosticCheck["status"]) => { + switch (status) { + case "checking": + return <Loader2 className="w-4 h-4 animate-spin text-blue-500" />; + case "success": + return <CheckCircle className="w-4 h-4 text-green-500" />; + case "error": + return <XCircle className="w-4 h-4 text-red-500" />; + case "warning": + return <AlertTriangle className="w-4 h-4 text-yellow-500" />; + } + }; + + const getStatusColor = (status: DiagnosticCheck["status"]) => { + switch (status) { + case "checking": + return "bg-blue-100 text-blue-800"; + case "success": + return "bg-green-100 text-green-800"; + case "error": + return "bg-red-100 text-red-800"; + case "warning": + return "bg-yellow-100 text-yellow-800"; + } + }; + + const hasErrors = checks.some(check => check.status === "error"); + + if (checks.length === 0) { + return null; + } + + return ( + <Card className={`border-2 ${hasErrors ? "border-red-200 bg-red-50" : "border-blue-200 bg-blue-50"}`}> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Database className="w-5 h-5 text-blue-600" /> + Supabase Connection Diagnostics + </CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div className="space-y-3"> + {checks.map((check, index) => ( + <div key={index} className="flex items-start justify-between p-3 bg-white rounded border"> + <div className="flex items-start gap-3"> + {getStatusIcon(check.status)} + <div> + <div className="font-medium text-sm">{check.name}</div> + <div className="text-sm text-gray-600">{check.message}</div> + {check.details && ( + <div className="text-xs text-gray-500 mt-1">{check.details}</div> + )} + </div> + </div> + <Badge className={getStatusColor(check.status)}> + {check.status} + </Badge> + </div> + ))} + </div> + + <div className="flex gap-2"> + <Button + size="sm" + onClick={runDiagnostics} + disabled={running} + className="flex items-center gap-2" + > + {running ? ( + <Loader2 className="w-4 h-4 animate-spin" /> + ) : ( + <Database className="w-4 h-4" /> + )} + {running ? "Running..." : "Run Diagnostics Again"} + </Button> + </div> + + {hasErrors && ( + <div className="bg-red-50 p-3 rounded border border-red-200"> + <p className="text-sm text-red-800 font-medium">⚠️ Issues Detected</p> + <p className="text-sm text-red-700 mt-1"> + Please check your Supabase configuration and database setup. + </p> + </div> + )} + </CardContent> + </Card> + ); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/components/ui/data-display/EnhancedChart.tsx b/website-monitoring-frontend/src/components/ui/data-display/EnhancedChart.tsx new file mode 100644 index 0000000..acb077e --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/data-display/EnhancedChart.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { motion } from "framer-motion"; +import { useMemo, useEffect, useState } from "react"; +import "@/lib/chart"; +import { ChartData, ChartOptions, TooltipItem } from "chart.js"; +import { SafeLineChart } from "@/components/ui/data-display/SafeLineChart"; + +interface Dataset { + label: string; + data: number[]; + color: string; + backgroundColor?: string; + borderWidth?: number; + fill?: boolean; + order?: number; +} + +interface EnhancedChartProps { + datasets: Dataset[]; + labels: string[]; + height?: number; + title?: string; + options?: Partial<ChartOptions<"line">>; +} + +export const EnhancedChart = ({ + datasets, + labels, + height = 300, + title, + options = {}, +}: EnhancedChartProps) => { + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + const chartKey = useMemo( + () => + `line-${labels.join("|")}-${datasets.map((d) => d.label).join("|")}`, + [labels, datasets], + ); + + const defaultOptions: ChartOptions<"line"> = { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: "nearest", + intersect: false, + axis: "x", + }, + animation: { + duration: 750, + easing: "easeInOutQuart", + }, + transitions: { + active: { + animation: { + duration: 750, + }, + }, + }, + scales: { + x: { + grid: { + display: false, + }, + ticks: { + color: "#6b7280", + maxRotation: 45, + minRotation: 45, + }, + }, + y: { + grid: { + color: "rgba(0,0,0,0.05)", + }, + ticks: { + color: "#6b7280", + }, + min: 0, + max: 100, + }, + }, + plugins: { + legend: { + position: "top" as const, + align: "start" as const, + labels: { + boxWidth: 10, + usePointStyle: true, + pointStyle: "circle", + }, + }, + tooltip: { + enabled: true, + backgroundColor: "white", + titleColor: "#111827", + bodyColor: "#374151", + borderColor: "#e5e7eb", + borderWidth: 1, + padding: 10, + callbacks: { + label: function (context: TooltipItem<"line">) { + if (context.dataset.label) { + return `${context.dataset.label}: ${context.parsed.y.toFixed(1)}`; + } + return `${context.parsed.y.toFixed(1)}`; + }, + }, + }, + }, + }; + + const chartData: ChartData<"line"> = { + labels, + datasets: datasets.map((dataset) => ({ + // Stable key to avoid canvas reuse issues + id: dataset.label, + label: dataset.label, + data: dataset.data, + borderColor: dataset.color, + backgroundColor: dataset.backgroundColor || "rgba(56, 189, 248, 0.1)", + borderWidth: dataset.borderWidth || 2, + fill: dataset.fill ?? false, + tension: 0.4, + pointRadius: 3, + pointHoverRadius: 6, + pointBackgroundColor: dataset.color, + pointBorderColor: "white", + pointBorderWidth: 2, + pointHoverBorderWidth: 2, + pointHoverBackgroundColor: dataset.color, + pointHoverBorderColor: "white", + })), + }; + + return ( + <motion.div + initial={{ y: 20, opacity: 0 }} + animate={{ y: 0, opacity: 1 }} + transition={{ duration: 0.5 }} + style={{ height }} + className="w-full" + > + {title && ( + <h3 className="text-lg font-semibold mb-4 text-gray-900">{title}</h3> + )} + <div className="relative w-full h-full"> + {mounted && labels.length > 0 && datasets.some((d) => d.data.length > 0) && ( + <SafeLineChart + key={chartKey} + data={chartData} + options={{ + ...defaultOptions, + ...options, + }} + /> + )} + </div> + </motion.div> + ); +}; diff --git a/website-monitoring-frontend/src/components/ui/data-display/Gauge.tsx b/website-monitoring-frontend/src/components/ui/data-display/Gauge.tsx new file mode 100644 index 0000000..b8dd62f --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/data-display/Gauge.tsx @@ -0,0 +1,92 @@ +import { motion } from "framer-motion"; +import { CircularProgressbar, buildStyles } from "react-circular-progressbar"; +import "react-circular-progressbar/dist/styles.css"; +import { useState, useEffect } from "react"; + +interface GaugeProps { + value: number; + title?: string; + size?: number; + duration?: number; + variant?: "animated" | "static"; + color?: string; +} + +export const Gauge = ({ + value, + title, + size = 120, + duration = 0.5, + variant = "animated", + color, +}: GaugeProps) => { + const [displayValue, setDisplayValue] = useState(0); + + useEffect(() => { + if (variant === "animated") { + setDisplayValue(value); + } + }, [value, variant]); + + const percentage = Math.round( + (variant === "animated" ? displayValue : value) * 100, + ); + + const getColor = () => { + if (color) return color; + return value >= 0.9 ? "#22c55e" : value >= 0.5 ? "#eab808" : "#ef4444"; + }; + + return ( + <motion.div + className="flex flex-col items-center" + initial={{ scale: 0.8, opacity: 0 }} + animate={{ scale: 1, opacity: 1 }} + transition={{ duration: 0.3 }} + > + {variant === "animated" ? ( + <motion.div + style={{ width: size, height: size }} + animate={{ rotate: [0, 10, 0] }} + transition={{ duration: 0.5, delay: duration }} + > + <CircularProgressbar + value={percentage} + text={`${percentage}%`} + styles={buildStyles({ + pathColor: getColor(), + textColor: getColor(), + trailColor: "#e5e7eb", + pathTransition: "stroke-dashoffset 0.5s ease-in-out", + })} + /> + </motion.div> + ) : ( + <div style={{ width: size, height: size }}> + <CircularProgressbar + value={percentage} + text={`${percentage}`} + styles={buildStyles({ + pathColor: getColor(), + textColor: getColor(), + trailColor: "#e5e7eb", + })} + /> + </div> + )} + {title && ( + <h3 className="mt-2 text-md font-medium text-gray-700">{title}</h3> + )} + </motion.div> + ); +}; + +// Optional: Export a pre-configured version for metrics +export const MetricsGauge = (props: Omit<GaugeProps, "variant">) => ( + <Gauge {...props} variant="animated" /> +); + +// Optional: Export a pre-configured version for static displays +export const StaticGauge = (props: Omit<GaugeProps, "variant">) => ( + <Gauge {...props} variant="static" /> +); diff --git a/website-monitoring-frontend/src/components/ui/data-display/PingDisplay.tsx b/website-monitoring-frontend/src/components/ui/data-display/PingDisplay.tsx new file mode 100644 index 0000000..57940ef --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/data-display/PingDisplay.tsx @@ -0,0 +1,88 @@ +import { useEffect, useState } from "react"; +import { AnimatePresence, LayoutGroup, motion, Variants } from "framer-motion"; + +const MAX_HISTORY = 5; + +type PingItem = { + id: number; + value: number; +}; + +const pingVariants: Variants = { + current: { + color: "#000000", + scale: 1.2, + transition: { duration: 0.4 }, + }, + row: { + color: "#6B7280", + scale: 0.8, + transition: { duration: 0.4 }, + }, +}; + +export const PingDisplay = ({ ping }: { ping: number }) => { + const [pings, setPings] = useState<PingItem[]>([]); + + useEffect(() => { + const newPing: PingItem = { id: Date.now(), value: ping }; + setPings((prev) => { + const updated = [newPing, ...prev]; + return updated.slice(0, MAX_HISTORY + 1); + }); + }, [ping]); + + return ( + <LayoutGroup> + <div className="relative w-72 h-48 bg-white"> + {pings[0] && ( + <AnimatePresence> + <motion.div + key={pings[0].id} + layoutId={`ping-${pings[0].id}`} + variants={pingVariants} + initial={false} + animate="current" + exit="row" + className="absolute top-8 left-1/2 -translate-x-1/2 text-3xl font-bold" + > + {pings[0].value} ms + </motion.div> + </AnimatePresence> + )} + + <p className="absolute top-20 left-1/2 -translate-x-1/2 text-sm text-gray-500"> + Current Ping + </p> + + <div + className="absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center" + style={{ height: "2rem", width: "250px", overflow: "hidden" }} + > + <AnimatePresence> + {pings.slice(1).map((item, index) => ( + <motion.div + key={item.id} + layoutId={`ping-${item.id}`} + variants={pingVariants} + initial={false} + animate="row" + exit={{ + opacity: 0, + x: 40, + transition: { duration: 0.4 }, + }} + className="absolute top-1/2 -translate-y-1/2 text-sm font-medium text-gray-600" + style={{ + left: `${index * 45}px`, + }} + > + {item.value} ms + </motion.div> + ))} + </AnimatePresence> + </div> + </div> + </LayoutGroup> + ); +}; diff --git a/website-monitoring-frontend/src/components/ui/data-display/SafeLineChart.tsx b/website-monitoring-frontend/src/components/ui/data-display/SafeLineChart.tsx new file mode 100644 index 0000000..d1b430d --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/data-display/SafeLineChart.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useEffect, useRef, useId } from "react"; +import { Chart as ChartJS, ChartData, ChartOptions } from "chart.js"; +import "@/lib/chart"; + +interface SafeLineChartProps { + data: ChartData<"line">; + options?: ChartOptions<"line">; + className?: string; + style?: React.CSSProperties; +} + +export function SafeLineChart({ data, options, className, style }: SafeLineChartProps) { + const canvasRef = useRef<HTMLCanvasElement | null>(null); + const chartRef = useRef<ChartJS<"line"> | null>(null); + const canvasId = useId(); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + // Skip Chart creation in test environment (jsdom cannot get canvas 2d context) + if (process.env.NODE_ENV === 'test') { + return () => {}; + } + + // Destroy any lingering instance bound to this canvas before creating a new one + try { + const existingByEl = ChartJS.getChart(canvas as unknown as HTMLCanvasElement); + existingByEl?.destroy(); + } catch {} + try { + const existingById = ChartJS.getChart(canvasId); + existingById?.destroy(); + } catch {} + + // Create new chart instance + chartRef.current = new ChartJS(canvas, { + type: "line", + data, + options, + }); + + return () => { + try { + chartRef.current?.destroy(); + } catch {} + chartRef.current = null; + }; + }, [JSON.stringify(data), JSON.stringify(options)]); + + return ( + <div className={className} style={style}> + <canvas id={canvasId} ref={canvasRef} /> + </div> + ); +} diff --git a/website-monitoring-frontend/src/components/ui/data-display/UptimeHeatmap.tsx b/website-monitoring-frontend/src/components/ui/data-display/UptimeHeatmap.tsx new file mode 100644 index 0000000..c800afc --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/data-display/UptimeHeatmap.tsx @@ -0,0 +1,53 @@ +export const UptimeHeatmap = ({ data }: { data: number[] }) => { + // We'll render them in ~5 rows of 7 columns each, or you can do something else. + const columns = 7; + const rows = Math.ceil(data.length / columns); + + const getColor = (val: number) => { + // If val is near 1.0 => darker green + // else degrade to lighter greens, yellows, or reds + if (val >= 0.95) return "bg-green-700"; + if (val >= 0.9) return "bg-green-500"; + if (val >= 0.8) return "bg-green-300"; + if (val >= 0.7) return "bg-yellow-300"; + if (val >= 0.5) return "bg-orange-300"; + return "bg-red-300"; + }; + + const grid: number[][] = []; + for (let r = 0; r < rows; r++) { + const rowSlice = data.slice(r * columns, r * columns + columns); + grid.push(rowSlice); + } + + return ( + <div className="flex flex-col space-y-2"> + {grid.map((row, rowIndex) => ( + <div key={rowIndex} className="flex space-x-2"> + {row.map((val, colIndex) => { + const dayNumber = rowIndex * columns + colIndex + 1; + return ( + <div + key={colIndex} + className={`w-8 h-8 rounded-sm transition-colors duration-500 ${getColor(val)}`} + > + {/* + We add a tooltip approach with group + absolutely positioned text + Or, a simpler approach: just a title attribute + => <div title={`Day ${dayNumber}: ${(val*100).toFixed(1)}%`}></div> + For something fancier, see below: + */} + <div className="group relative w-full h-full"> + <div + className="absolute inset-0" + title={`Day ${dayNumber}: ${(val * 100).toFixed(1)}%`} + ></div> + </div> + </div> + ); + })} + </div> + ))} + </div> + ); +}; diff --git a/website-monitoring-frontend/src/components/ui/data-display/__tests__/SafeLineChart.test.tsx b/website-monitoring-frontend/src/components/ui/data-display/__tests__/SafeLineChart.test.tsx new file mode 100644 index 0000000..42d85f8 --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/data-display/__tests__/SafeLineChart.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from '@testing-library/react' +import { SafeLineChart } from '@/components/ui/data-display/SafeLineChart' + +describe('SafeLineChart', () => { + it('renders a canvas', () => { + render( + <SafeLineChart + data={{ labels: ['A'], datasets: [{ id: 'd1' as any, label: 'L', data: [1] }] as any }} + options={{ responsive: true }} + /> + ) + + const canvases = document.getElementsByTagName('canvas') + expect(canvases.length).toBeGreaterThan(0) + }) +}) diff --git a/website-monitoring-frontend/src/components/ui/feedback/Alert.tsx b/website-monitoring-frontend/src/components/ui/feedback/Alert.tsx new file mode 100644 index 0000000..3b8ee79 --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/feedback/Alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) { + return ( + <div + data-slot="alert" + role="alert" + className={cn(alertVariants({ variant }), className)} + {...props} + /> + ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="alert-title" + className={cn( + "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", + className + )} + {...props} + /> + ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + <div + data-slot="alert-description" + className={cn( + "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed", + className + )} + {...props} + /> + ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/website-monitoring-frontend/src/components/ui/feedback/ErrorBoundary.tsx b/website-monitoring-frontend/src/components/ui/feedback/ErrorBoundary.tsx new file mode 100644 index 0000000..156b8cc --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/feedback/ErrorBoundary.tsx @@ -0,0 +1,81 @@ +"use client"; + +import React, { Component, ErrorInfo, ReactNode } from "react"; +import { Button } from "@/components/ui/forms/Button"; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component<Props, State> { + public state: State = { + hasError: false, + }; + + public static getDerivedStateFromError(error: Error): State { + // Update state so the next render will show the fallback UI. + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("Uncaught error:", error, errorInfo); + } + + public render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( + <div className="min-h-screen flex items-center justify-center bg-gray-50"> + <div className="text-center p-8 max-w-md"> + <h2 className="text-2xl font-bold text-gray-900 mb-4"> + Oops! Something went wrong + </h2> + <p className="text-gray-600 mb-6"> + {this.state.error?.message || "An unexpected error occurred"} + </p> + <div className="space-y-4"> + <Button + onClick={() => window.location.reload()} + className="w-full" + > + Refresh Page + </Button> + <Button + variant="outline" + onClick={() => this.setState({ hasError: false })} + className="w-full" + > + Try Again + </Button> + </div> + </div> + </div> + ); + } + + return this.props.children; + } +} + +// You can also create a custom hook to use the ErrorBoundary +export function withErrorBoundary<P extends object>( + WrappedComponent: React.ComponentType<P>, + fallback?: ReactNode, +) { + return function WithErrorBoundary(props: P) { + return ( + <ErrorBoundary fallback={fallback}> + <WrappedComponent {...props} /> + </ErrorBoundary> + ); + }; +} diff --git a/website-monitoring-frontend/src/components/ui/feedback/LoadingSpinner.tsx b/website-monitoring-frontend/src/components/ui/feedback/LoadingSpinner.tsx new file mode 100644 index 0000000..4288d65 --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/feedback/LoadingSpinner.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { motion } from "framer-motion"; + +interface LoadingSpinnerProps { + size?: number; + className?: string; +} + +export const LoadingSpinner = ({ + size = 40, + className = "", +}: LoadingSpinnerProps) => { + return ( + <div className="fixed inset-0 flex items-center justify-center bg-white/80 backdrop-blur-sm z-50"> + <motion.div + className={`inline-block ${className}`} + style={{ + width: size, + height: size, + border: "3px solid #ddd", + borderTop: "3px solid #3b82f6", + borderRadius: "50%", + }} + animate={{ + rotate: 360, + }} + transition={{ + duration: 1, + repeat: Infinity, + ease: "linear", + }} + /> + </div> + ); +}; diff --git a/website-monitoring-frontend/src/components/ui/feedback/Progress.tsx b/website-monitoring-frontend/src/components/ui/feedback/Progress.tsx new file mode 100644 index 0000000..088967a --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/feedback/Progress.tsx @@ -0,0 +1,43 @@ +"use client"; + +import * as React from "react"; +import * as ProgressPrimitive from "@radix-ui/react-progress"; + +import { cn } from "@/lib/utils"; + +interface ProgressProps + extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> { + value: number; + max?: number; + className?: string; + indicatorClassName?: string; +} + +const Progress = React.forwardRef< + React.ElementRef<typeof ProgressPrimitive.Root>, + ProgressProps +>(({ className, value, max = 100, indicatorClassName, ...props }, ref) => { + const percentage = Math.min(100, Math.max(0, (value / max) * 100)); + + return ( + <ProgressPrimitive.Root + ref={ref} + className={cn( + "relative h-2 w-full overflow-hidden rounded-full bg-gray-100", + className, + )} + {...props} + > + <ProgressPrimitive.Indicator + className={cn( + "h-full w-full flex-1 bg-blue-600 transition-all", + indicatorClassName, + )} + style={{ transform: `translateX(-${100 - percentage}%)` }} + /> + </ProgressPrimitive.Root> + ); +}); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; diff --git a/website-monitoring-frontend/src/components/ui/forms/Button.tsx b/website-monitoring-frontend/src/components/ui/forms/Button.tsx new file mode 100644 index 0000000..cd0857a --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/forms/Button.tsx @@ -0,0 +1,58 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", + outline: + "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps<typeof buttonVariants> & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + <Comp + data-slot="button" + className={cn(buttonVariants({ variant, size, className }))} + {...props} + /> + ) +} + +export { Button, buttonVariants } diff --git a/website-monitoring-frontend/src/components/ui/forms/Checkbox.tsx b/website-monitoring-frontend/src/components/ui/forms/Checkbox.tsx new file mode 100644 index 0000000..42e2c1b --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/forms/Checkbox.tsx @@ -0,0 +1,30 @@ +"use client"; + +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Checkbox = React.forwardRef< + React.ElementRef<typeof CheckboxPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> +>(({ className, ...props }, ref) => ( + <CheckboxPrimitive.Root + ref={ref} + className={cn( + "peer h-4 w-4 shrink-0 rounded-sm border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600", + className, + )} + {...props} + > + <CheckboxPrimitive.Indicator + className={cn("flex items-center justify-center")} + > + <Check className="h-3 w-3 text-white" /> + </CheckboxPrimitive.Indicator> + </CheckboxPrimitive.Root> +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/website-monitoring-frontend/src/components/ui/forms/Form.tsx b/website-monitoring-frontend/src/components/ui/forms/Form.tsx new file mode 100644 index 0000000..0ee1e55 --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/forms/Form.tsx @@ -0,0 +1,168 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, + useFormState, +} from "react-hook-form"; + +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/forms/Label"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext<FormFieldContextValue>( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, +>({ + ...props +}: ControllerProps<TFieldValues, TName>) => { + return ( + <FormFieldContext.Provider value={{ name: props.name }}> + <Controller {...props} /> + </FormFieldContext.Provider> + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState } = useFormContext(); + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within <FormField>"); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext<FormItemContextValue>( + {} as FormItemContextValue, +); + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId(); + + return ( + <FormItemContext.Provider value={{ id }}> + <div + data-slot="form-item" + className={cn("grid gap-2", className)} + {...props} + /> + </FormItemContext.Provider> + ); +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps<typeof LabelPrimitive.Root>) { + const { error, formItemId } = useFormField(); + + return ( + <Label + data-slot="form-label" + data-error={!!error} + className={cn("data-[error=true]:text-destructive-foreground", className)} + htmlFor={formItemId} + {...props} + /> + ); +} + +function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { + const { error, formItemId, formDescriptionId, formMessageId } = + useFormField(); + + return ( + <Slot + data-slot="form-control" + id={formItemId} + aria-describedby={ + !error + ? `${formDescriptionId}` + : `${formDescriptionId} ${formMessageId}` + } + aria-invalid={!!error} + {...props} + /> + ); +} + +function FormDescription({ className, ...props }: React.ComponentProps<"p">) { + const { formDescriptionId } = useFormField(); + + return ( + <p + data-slot="form-description" + id={formDescriptionId} + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ); +} + +function FormMessage({ className, ...props }: React.ComponentProps<"p">) { + const { error, formMessageId } = useFormField(); + const body = error ? String(error?.message ?? "") : props.children; + + if (!body) { + return null; + } + + return ( + <p + data-slot="form-message" + id={formMessageId} + className={cn("text-destructive-foreground text-sm", className)} + {...props} + > + {body} + </p> + ); +} + +export { + useFormField, + Form, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, + FormField, +}; diff --git a/website-monitoring-frontend/src/components/ui/forms/Input.tsx b/website-monitoring-frontend/src/components/ui/forms/Input.tsx new file mode 100644 index 0000000..4bfe9cc --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/forms/Input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + <input + type={type} + data-slot="input" + className={cn( + "border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", + "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + className + )} + {...props} + /> + ) +} + +export { Input } diff --git a/website-monitoring-frontend/src/components/ui/forms/Label.tsx b/website-monitoring-frontend/src/components/ui/forms/Label.tsx new file mode 100644 index 0000000..fb5fbc3 --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/forms/Label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps<typeof LabelPrimitive.Root>) { + return ( + <LabelPrimitive.Root + data-slot="label" + className={cn( + "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", + className + )} + {...props} + /> + ) +} + +export { Label } diff --git a/website-monitoring-frontend/src/components/ui/forms/Progress.tsx b/website-monitoring-frontend/src/components/ui/forms/Progress.tsx new file mode 100644 index 0000000..a17d8fc --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/forms/Progress.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { forwardRef } from "react"; +import { cn } from "@/lib/utils"; + +export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> { + value?: number; + max?: number; + className?: string; +} + +const Progress = forwardRef<HTMLDivElement, ProgressProps>( + ({ className, value = 0, max = 100, ...props }, ref) => { + const percentage = Math.min(Math.max((value / max) * 100, 0), 100); + + return ( + <div + ref={ref} + className={cn( + "relative h-2 w-full overflow-hidden rounded-full bg-gray-200", + className + )} + {...props} + > + <div + className="h-full w-full flex-1 bg-blue-600 transition-all duration-300 ease-in-out" + style={{ + transform: `translateX(-${100 - percentage}%)`, + }} + /> + </div> + ); + } +); + +Progress.displayName = "Progress"; + +export { Progress }; diff --git a/website-monitoring-frontend/src/components/ui/layout/Badge.tsx b/website-monitoring-frontend/src/components/ui/layout/Badge.tsx new file mode 100644 index 0000000..22d60ab --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/layout/Badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + success: + "border-transparent bg-green-100 text-green-800 hover:bg-green-200", + green: + "border-transparent bg-green-100 text-green-800 hover:bg-green-200", + blue: "border-transparent bg-blue-100 text-blue-800 hover:bg-blue-200", + red: "border-transparent bg-red-100 text-red-800 hover:bg-red-200", + yellow: + "border-transparent bg-yellow-100 text-yellow-800 hover:bg-yellow-200", + gray: "border-transparent bg-gray-100 text-gray-800 hover:bg-gray-200", + purple: + "border-transparent bg-purple-100 text-purple-800 hover:bg-purple-200", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes<HTMLDivElement>, + VariantProps<typeof badgeVariants> {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( + <div className={cn(badgeVariants({ variant }), className)} {...props} /> + ); +} + +export { Badge, badgeVariants }; diff --git a/website-monitoring-frontend/src/components/ui/layout/Card.tsx b/website-monitoring-frontend/src/components/ui/layout/Card.tsx new file mode 100644 index 0000000..5e960a6 --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/layout/Card.tsx @@ -0,0 +1,68 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card" + className={cn( + "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-header" + className={cn("flex flex-col gap-1.5 px-6", className)} + {...props} + /> + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-title" + className={cn("leading-none font-semibold", className)} + {...props} + /> + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-description" + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-content" + className={cn("px-6", className)} + {...props} + /> + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-footer" + className={cn("flex items-center px-6", className)} + {...props} + /> + ) +} + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/website-monitoring-frontend/src/components/ui/layout/Tabs.tsx b/website-monitoring-frontend/src/components/ui/layout/Tabs.tsx new file mode 100644 index 0000000..cbef1a4 --- /dev/null +++ b/website-monitoring-frontend/src/components/ui/layout/Tabs.tsx @@ -0,0 +1,65 @@ +"use client"; + +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import { cn } from "@/lib/utils"; + +function Tabs({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Root>) { + return ( + <TabsPrimitive.Root + data-slot="tabs" + className={cn("flex flex-col gap-2", className)} + {...props} + /> + ); +} + +function TabsList({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.List>) { + return ( + <TabsPrimitive.List + data-slot="tabs-list" + className={cn( + "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-1", + className + )} + {...props} + /> + ); +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Trigger>) { + return ( + <TabsPrimitive.Trigger + data-slot="tabs-trigger" + className={cn( + "data-[state=active]:bg-background data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring inline-flex flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + className + )} + {...props} + /> + ); +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Content>) { + return ( + <TabsPrimitive.Content + data-slot="tabs-content" + className={cn("flex-1 outline-none", className)} + {...props} + /> + ); +} + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/website-monitoring-frontend/src/constants/colors.ts b/website-monitoring-frontend/src/constants/colors.ts new file mode 100644 index 0000000..38a8b73 --- /dev/null +++ b/website-monitoring-frontend/src/constants/colors.ts @@ -0,0 +1,53 @@ +export const COLORS = { + metrics: { + performance: "rgba(59, 130, 246, 1)", // blue + seo: "rgba(16, 185, 129, 1)", // green + accessibility: "rgba(249, 115, 22, 1)", // orange + bestPractices: "rgba(139, 92, 246, 1)", // purple + }, + competitors: [ + "rgba(59, 130, 246, 1)", // blue + "rgba(16, 185, 129, 1)", // green + "rgba(249, 115, 22, 1)", // orange + "rgba(139, 92, 246, 1)", // purple + "rgba(236, 72, 153, 1)", // pink + "rgba(234, 179, 8, 1)", // yellow + ], + severity: { + high: "#ef4444", + medium: "#eab808", + low: "#22c55e", + }, + chart: { + background: { + blue: "rgba(59, 130, 246, 0.1)", + green: "rgba(16, 185, 129, 0.1)", + orange: "rgba(249, 115, 22, 0.1)", + purple: "rgba(139, 92, 246, 0.1)", + }, + border: { + blue: "rgba(59, 130, 246, 1)", + green: "rgba(16, 185, 129, 1)", + orange: "rgba(249, 115, 22, 1)", + purple: "rgba(139, 92, 246, 1)", + }, + }, + status: { + success: "bg-green-500", + warning: "bg-yellow-500", + error: "bg-red-500", + info: "bg-blue-500", + }, +} as const; + +export type MetricColorKey = keyof typeof COLORS.metrics; +export type SeverityColorKey = keyof typeof COLORS.severity; +export type StatusColorKey = keyof typeof COLORS.status; + +export const getMetricColor = (metric: MetricColorKey) => + COLORS.metrics[metric]; +export const getSeverityColor = (severity: SeverityColorKey) => + COLORS.severity[severity]; +export const getStatusColor = (status: StatusColorKey) => COLORS.status[status]; +export const getCompetitorColor = (index: number) => + COLORS.competitors[index % COLORS.competitors.length]; diff --git a/website-monitoring-frontend/src/contexts/AuthContext.tsx b/website-monitoring-frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..9ea17db --- /dev/null +++ b/website-monitoring-frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,378 @@ +import { createContext, useContext, useEffect, useState } from "react"; +import { User } from "@supabase/supabase-js"; +import { supabase } from "@/lib/supabase"; +import { useRouter, usePathname } from "next/navigation"; +import { LoadingSpinner } from "@/components/ui/feedback/LoadingSpinner"; + +interface UserDetails { + id: string; + email: string; + name: string; + organization_id: string; + role: string; +} + +interface AuthContextType { + user: User | null; + userDetails: UserDetails | null; + loading: boolean; + signUp: ( + email: string, + password: string, + name: string, + ) => Promise<{ needsEmailVerification: boolean } | undefined>; + signIn: (email: string, password: string) => Promise<void>; + signOut: () => Promise<void>; + createOrganizationForUser: ( + userId: string, + orgName: string, + ) => Promise<string | null>; +} + +const AuthContext = createContext<AuthContextType | undefined>(undefined); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState<User | null>(null); + const [userDetails, setUserDetails] = useState<UserDetails | null>(null); + const [loading, setLoading] = useState(true); + const router = useRouter(); + const pathname = usePathname(); + + // Define the authentication functions + const signUp = async (email: string, password: string, name: string) => { + try { + // Step 1: Create organization first + const { data: org, error: orgError } = await supabase + .from("organizations") + .insert([ + { + name: `${name}'s Organization`, + subscription_tier: "free", + subscription_status: "active", + }, + ]) + .select() + .single(); + + if (orgError) throw orgError; + const organizationId = org.id; + + // Step 2: Sign up the user with Auth + const { data: authData, error: signUpError } = await supabase.auth.signUp( + { + email, + password, + options: { + emailRedirectTo: `${window.location.origin}/auth/callback`, + data: { + name, + organization_id: organizationId, + role: "owner", + }, + }, + }, + ); + + if (signUpError) throw signUpError; + if (!authData.user) throw new Error("User account creation failed"); + + // Step 3: Create user record in the database + const { error: createUserError } = await supabase.from("users").insert([ + { + id: authData.user.id, + email, + name, + organization_id: organizationId, + role: "owner", + }, + ]); + + if (createUserError) throw createUserError; + + return { needsEmailVerification: true }; + } catch (error) { + console.error("Signup error:", error); + throw error; + } + }; + + const signIn = async (email: string, password: string) => { + try { + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + if (error) throw error; + } catch (error) { + console.error("Sign-in error:", error); + throw error; + } + }; + + const signOut = async () => { + try { + const { error } = await supabase.auth.signOut(); + if (error) throw error; + + // Clear user state + setUser(null); + setUserDetails(null); + + // Redirect to home page after sign out + router.push("/"); + } catch (error) { + console.error("Sign-out error:", error); + throw error; + } + }; + + const createOrganizationForUser = async (userId: string, orgName: string) => { + try { + // 1. Create organization + const { data: org, error: orgError } = await supabase + .from("organizations") + .insert([ + { + name: orgName, + subscription_tier: "free", + subscription_status: "active", + }, + ]) + .select() + .single(); + + if (orgError) throw orgError; + + // 2. Update user with organization ID + const { error: updateError } = await supabase + .from("users") + .update({ organization_id: org.id }) + .eq("id", userId); + + if (updateError) throw updateError; + + // 3. Update local state + setUserDetails((prev) => prev ? { ...prev, organization_id: org.id } : null); + + console.log("Created organization and assigned to user:", org.id); + return org.id; + } catch (error) { + console.error("Failed to create organization:", error); + return null; + } + }; + + useEffect(() => { + async function loadUserData() { + try { + // Get auth session + const { + data: { session }, + } = await supabase.auth.getSession(); + + if (session?.user) { + // Set the authenticated user + setUser(session.user); + + // Fetch additional user details including organization_id + const { data: userData, error: userError } = await supabase + .from("users") + .select("*") + .eq("id", session.user.id) + .single(); + + if (!userError && userData) { + setUserDetails(userData); + + // If user has no organization, create one + if (!userData.organization_id) { + console.log("No organization found, creating one..."); + try { + await createOrganizationForUser( + session.user.id, + userData.name || "My Organization", + ); + } catch (orgError) { + console.error("Failed to create organization:", orgError); + // Continue anyway, don't block the app + } + } + } else { + console.error("Failed to load user details:", { + error: userError, + message: userError?.message, + code: userError?.code, + details: userError?.details, + hint: userError?.hint + }); + // Only redirect to fix-account for specific errors + if (userError && userError.code === 'PGRST116') { + router.push("/auth/fix-account"); + } + } + } else { + // No session, user is not logged in + setUser(null); + setUserDetails(null); + } + } catch (error) { + console.error("Error loading user data:", error); + // Don't redirect on errors, just log them + } finally { + // Set loading to false only on initial load + setLoading(false); + } + } + + // Only run initial load if loading is true (initial state) + if (loading) { + loadUserData(); + } + + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange(async (event, session) => { + try { + console.log("Auth state change:", event, !!session); + + setUser(session?.user ?? null); + + // Only load user details on SIGNED_IN event, not on every auth state change + if (event === "SIGNED_IN" && session?.user && !userDetails) { + // Fetch user details when auth state changes + const { data: userData, error: userError } = await supabase + .from("users") + .select("*") + .eq("id", session.user.id) + .single(); + + if (!userError && userData) { + setUserDetails(userData); + + // If user has no organization, create one + if (!userData.organization_id) { + console.log("No organization found, creating one..."); + try { + await createOrganizationForUser( + session.user.id, + userData.name || session.user.user_metadata?.full_name || "My Organization", + ); + } catch (orgError) { + console.error("Failed to create organization:", orgError); + } + } + } else if (userError && userError.code === 'PGRST116') { + // User doesn't exist in our users table (likely OAuth user) + console.log("Creating user profile for OAuth user..."); + + try { + // Create organization first + const { data: org, error: orgError } = await supabase + .from("organizations") + .insert([ + { + name: `${session.user.user_metadata?.full_name || 'User'}'s Organization`, + subscription_tier: "free", + subscription_status: "active", + }, + ]) + .select() + .single(); + + if (!orgError && org) { + // Create user record + const { error: createUserError } = await supabase.from("users").insert([ + { + id: session.user.id, + email: session.user.email, + name: session.user.user_metadata?.full_name || session.user.email?.split('@')[0] || 'User', + organization_id: org.id, + role: "owner", + }, + ]); + + if (!createUserError) { + // Fetch the newly created user details + const { data: newUserData } = await supabase + .from("users") + .select("*") + .eq("id", session.user.id) + .single(); + + if (newUserData) { + setUserDetails(newUserData); + } + } + } + } catch (createError) { + console.error("Failed to create OAuth user profile:", createError); + } + } else { + console.error("Failed to load user details:", userError); + } + } else if (event === "SIGNED_OUT") { + setUserDetails(null); + } + + // Navigation logic - only navigate if not already on the target route + if (event === "SIGNED_IN" && !pathname.startsWith("/dashboard")) { + router.push("/dashboard"); + } else if (event === "SIGNED_OUT" && pathname !== "/") { + router.push("/"); + } + } catch (error) { + console.error("Error in auth state change:", error); + } + // Don't set loading to false here - it causes navigation loading issues + }); + + return () => subscription.unsubscribe(); + }, [router, pathname]); + + // Add route protection + useEffect(() => { + const protectedRoutes = ["/dashboard"]; + const publicOnlyRoutes = ["/auth"]; + + // Don't redirect during initial loading + if (loading) return; + + if (!user && protectedRoutes.some(route => pathname.startsWith(route))) { + router.push("/auth"); + } else if (user && publicOnlyRoutes.some(route => pathname.startsWith(route))) { + router.push("/dashboard"); + } + }, [user, loading, pathname, router]); + + // Only show loading spinner on very first load and only for auth pages + const shouldShowLoading = loading && pathname.startsWith("/auth") && !user; + + if (shouldShowLoading) { + return <LoadingSpinner />; + } + + return ( + <AuthContext.Provider + value={{ + user, + userDetails, + loading, + signUp, + signIn, + signOut, + createOrganizationForUser, + }} + > + {children} + </AuthContext.Provider> + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/website-monitoring-frontend/src/contexts/MonitoringContext.tsx b/website-monitoring-frontend/src/contexts/MonitoringContext.tsx new file mode 100644 index 0000000..25f9f5e --- /dev/null +++ b/website-monitoring-frontend/src/contexts/MonitoringContext.tsx @@ -0,0 +1,201 @@ +"use client"; + +import { createContext, useContext, useEffect, useState } from "react"; +import { supabase } from "@/lib/supabase"; +import type { Scan, Alert, MetricValue } from "@/types/monitoring"; + +interface MonitoringContextType { + latestScan: Scan | null; + alerts: Alert[]; + metrics: MetricValue[]; + isMonitoring: boolean; + startMonitoring: (websiteId: string) => Promise<void>; + stopMonitoring: () => void; +} + +const MonitoringContext = createContext<MonitoringContextType | undefined>( + undefined, +); + +export function MonitoringProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [websiteId, setWebsiteId] = useState<string | null>(null); + const [latestScan, setLatestScan] = useState<Scan | null>(null); + const [alerts, setAlerts] = useState<Alert[]>([]); + const [metrics, setMetrics] = useState<MetricValue[]>([]); + const [isMonitoring, setIsMonitoring] = useState(false); + + useEffect(() => { + if (websiteId) { + setupRealtimeSubscriptions(websiteId); + } + return () => { + cleanupSubscriptions(); + }; + }, [websiteId]); + + const setupRealtimeSubscriptions = (websiteId: string) => { + const scanSubscription = supabase + .channel("scans") + .on( + "postgres_changes", + { + event: "*", + schema: "public", + table: "scans", + filter: `website_id=eq.${websiteId}`, + }, + handleScanUpdate, + ) + .subscribe(); + + const alertSubscription = supabase + .channel("alerts") + .on( + "postgres_changes", + { + event: "*", + schema: "public", + table: "alerts", + filter: `website_id=eq.${websiteId}`, + }, + handleAlertUpdate, + ) + .subscribe(); + + const metricSubscription = supabase + .channel("metrics") + .on( + "postgres_changes", + { + event: "INSERT", + schema: "public", + table: "metric_values", + filter: `scan_id=eq.${latestScan?.id}`, + }, + handleMetricUpdate, + ) + .subscribe(); + + return () => { + scanSubscription.unsubscribe(); + alertSubscription.unsubscribe(); + metricSubscription.unsubscribe(); + }; + }; + + const handleScanUpdate = (payload: any) => { + const scan = payload.new as Scan; + setLatestScan(scan); + }; + + const handleAlertUpdate = (payload: any) => { + const alert = payload.new as Alert; + setAlerts((prev) => [...prev, alert]); + }; + + const handleMetricUpdate = (payload: any) => { + const metric = payload.new as MetricValue; + setMetrics((prev) => [...prev, metric]); + }; + + const startMonitoring = async (websiteId: string) => { + try { + setWebsiteId(websiteId); + setIsMonitoring(true); + + // Load initial data + await loadInitialData(websiteId); + + // Initialize monitoring + const response = await fetch("/api/monitor/start", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ websiteId }), + }); + + if (!response.ok) { + throw new Error("Failed to start monitoring"); + } + } catch (error) { + console.error("Monitoring error:", error); + setIsMonitoring(false); + } + }; + + const stopMonitoring = () => { + setWebsiteId(null); + setIsMonitoring(false); + cleanupSubscriptions(); + }; + + const loadInitialData = async (websiteId: string) => { + try { + // Load latest scan + const { data: scanData } = await supabase + .from("scans") + .select("*") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }) + .limit(1) + .single(); + + if (scanData) { + setLatestScan(scanData); + + // Load metrics for latest scan + const { data: metricData } = await supabase + .from("metric_values") + .select("*") + .eq("scan_id", scanData.id); + + if (metricData) { + setMetrics(metricData); + } + } + + // Load active alerts + const { data: alertData } = await supabase + .from("alerts") + .select("*") + .eq("website_id", websiteId) + .eq("status", "open"); + + if (alertData) { + setAlerts(alertData); + } + } catch (error) { + console.error("Failed to load initial data:", error); + } + }; + + const cleanupSubscriptions = () => { + supabase.removeAllChannels(); + }; + + return ( + <MonitoringContext.Provider + value={{ + latestScan, + alerts, + metrics, + isMonitoring, + startMonitoring, + stopMonitoring, + }} + > + {children} + </MonitoringContext.Provider> + ); +} + +export function useMonitoring() { + const context = useContext(MonitoringContext); + if (context === undefined) { + throw new Error("useMonitoring must be used within a MonitoringProvider"); + } + return context; +} diff --git a/website-monitoring-frontend/src/hooks/useDashboardData.ts b/website-monitoring-frontend/src/hooks/useDashboardData.ts new file mode 100644 index 0000000..fdb555d --- /dev/null +++ b/website-monitoring-frontend/src/hooks/useDashboardData.ts @@ -0,0 +1,45 @@ +import { useAuth } from "@/contexts/AuthContext"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +interface DashboardDataHookOptions { + requireOrganization?: boolean; + redirectTo?: string; +} + +export function useDashboardData(options: DashboardDataHookOptions = {}) { + const { user, userDetails } = useAuth(); + const router = useRouter(); + const { requireOrganization = true, redirectTo = "/auth" } = options; + + useEffect(() => { + // If not authenticated, redirect + if (user === null) { + router.push(redirectTo); + return; + } + }, [user, router, redirectTo]); + + const isLoading = user === null; + const isAuthenticated = !!user; + const hasUserDetails = !!userDetails; + const hasOrganization = !!userDetails?.organization_id; + + // Determine if we should show loading + const shouldShowLoading = isLoading || (requireOrganization && !hasUserDetails); + + // Determine if we should show no-organization message + const shouldShowNoOrganization = hasUserDetails && requireOrganization && !hasOrganization; + + return { + user, + userDetails, + organizationId: userDetails?.organization_id, + isLoading, + isAuthenticated, + hasUserDetails, + hasOrganization, + shouldShowLoading, + shouldShowNoOrganization, + }; +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/hooks/useProtectedRoute.ts b/website-monitoring-frontend/src/hooks/useProtectedRoute.ts new file mode 100644 index 0000000..ef277bb --- /dev/null +++ b/website-monitoring-frontend/src/hooks/useProtectedRoute.ts @@ -0,0 +1,24 @@ +import { useAuth } from "@/contexts/AuthContext"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +export function useProtectedRoute() { + const { user, userDetails } = useAuth(); + const router = useRouter(); + + useEffect(() => { + // If not authenticated, redirect to auth + if (user === null) { + router.push("/auth"); + return; + } + }, [user, router]); + + return { + user, + userDetails, + isLoading: user === null || userDetails === null, + isAuthenticated: !!user, + hasOrganization: !!userDetails?.organization_id, + }; +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/hooks/useRealtimeMonitoring.ts b/website-monitoring-frontend/src/hooks/useRealtimeMonitoring.ts new file mode 100644 index 0000000..33e3375 --- /dev/null +++ b/website-monitoring-frontend/src/hooks/useRealtimeMonitoring.ts @@ -0,0 +1,176 @@ +import { useEffect, useState } from "react"; +import { supabase } from "@/lib/supabase"; +import type { Scan, Page } from "@/types/database"; + +export function useRealtimeMonitoring(websiteId: string) { + const [scans, setScans] = useState<Scan[]>([]); + const [pages, setPages] = useState<Page[]>([]); + const [progress, setProgress] = useState({ + pagesDiscovered: 0, + pagesAnalyzed: 0, + currentStatus: "idle", + crawlProgress: 0, // Add crawl progress tracking + }); + + useEffect(() => { + // Initial data fetch + fetchInitialData(); + + // Set up realtime subscriptions + const scanSubscription = supabase + .channel("scans") + .on( + "postgres_changes", + { + event: "*", + schema: "public", + table: "scans", + filter: `website_id=eq.${websiteId}`, + }, + (payload) => { + if (payload.new) { + handleScanUpdate(payload.new as Scan); + } + }, + ) + .subscribe(); + + const pageSubscription = supabase + .channel("pages") + .on( + "postgres_changes", + { + event: "INSERT", + schema: "public", + table: "pages", + filter: `website_id=eq.${websiteId}`, + }, + (payload) => { + if (payload.new) { + handleNewPage(payload.new as Page); + } + }, + ) + .subscribe(); + + // Add crawl sessions subscription for real-time progress + const crawlSessionSubscription = supabase + .channel("crawl_sessions") + .on( + "postgres_changes", + { + event: "UPDATE", + schema: "public", + table: "crawl_sessions", + filter: `website_id=eq.${websiteId}`, + }, + (payload) => { + if (payload.new) { + handleCrawlSessionUpdate(payload.new); + } + }, + ) + .subscribe(); + + return () => { + scanSubscription.unsubscribe(); + pageSubscription.unsubscribe(); + crawlSessionSubscription.unsubscribe(); + }; + }, [websiteId]); + + const fetchInitialData = async () => { + // Fetch existing scans + const { data: existingScans } = await supabase + .from("scans") + .select("*") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }); + + if (existingScans) { + setScans(existingScans); + } + + // Fetch discovered pages + const { data: existingPages } = await supabase + .from("pages") + .select("*") + .eq("website_id", websiteId); + + if (existingPages) { + setPages(existingPages); + } + + // Fetch latest crawl session for progress tracking + const { data: latestSession } = await supabase + .from("crawl_sessions") + .select("*") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }) + .limit(1) + .single(); + + if (latestSession) { + const discovered = latestSession.pages_discovered || 0; + const processed = latestSession.pages_processed || 0; + const crawlProgress = discovered > 0 ? Math.round((processed / discovered) * 100) : 0; + + setProgress((prev) => ({ + ...prev, + pagesDiscovered: discovered, + crawlProgress: crawlProgress, + currentStatus: latestSession.status || "idle", + pagesAnalyzed: existingScans?.filter((s) => s.status === "completed").length || 0, + })); + } else { + setProgress((prev) => ({ + ...prev, + pagesDiscovered: existingPages?.length || 0, + pagesAnalyzed: existingScans?.filter((s) => s.status === "completed").length || 0, + })); + } + }; + + const handleScanUpdate = (scan: Scan) => { + setScans((prev) => { + const index = prev.findIndex((s) => s.id === scan.id); + if (index >= 0) { + const updated = [...prev]; + updated[index] = scan; + return updated; + } + return [scan, ...prev]; + }); + + setProgress((prev) => ({ + ...prev, + pagesAnalyzed: scans.filter((s) => s.status === "completed").length, + currentStatus: scan.status, + })); + }; + + const handleCrawlSessionUpdate = (crawlSession: any) => { + if (!crawlSession) return; + + const discovered = crawlSession.pages_discovered || 0; + const processed = crawlSession.pages_processed || 0; + const crawlProgress = discovered > 0 ? Math.round((processed / discovered) * 100) : 0; + + setProgress((prev) => ({ + ...prev, + pagesDiscovered: discovered, + crawlProgress: crawlProgress, + currentStatus: crawlSession.status || prev.currentStatus, + })); + }; + + const handleNewPage = (page: Page) => { + setPages((prev) => [...prev, page]); + setProgress((prev) => ({ + ...prev, + pagesDiscovered: prev.pagesDiscovered + 1, + })); + }; + + return { scans, pages, progress }; +} diff --git a/website-monitoring-frontend/src/hooks/useSupabaseAuth.ts b/website-monitoring-frontend/src/hooks/useSupabaseAuth.ts new file mode 100644 index 0000000..e23e74d --- /dev/null +++ b/website-monitoring-frontend/src/hooks/useSupabaseAuth.ts @@ -0,0 +1,22 @@ +import { useCallback } from "react"; +import { supabase } from "@/lib/supabase"; + +export const useSupabaseAuth = () => { + const signUp = useCallback( + async (email: string, password: string, name: string) => { + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: { name }, + }, + }); + + if (error) throw error; + return data; + }, + [], + ); + + return { signUp }; +}; diff --git a/website-monitoring-frontend/src/lib/admin.ts b/website-monitoring-frontend/src/lib/admin.ts new file mode 100644 index 0000000..bbe6cc6 --- /dev/null +++ b/website-monitoring-frontend/src/lib/admin.ts @@ -0,0 +1,32 @@ +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; +// This is a server-side only key - never expose to the client +const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + +// Only create the admin client if we're on the server side and have the service key +let supabaseAdmin: ReturnType<typeof createClient> | null = null; + +if (typeof window === 'undefined' && supabaseServiceKey) { + // We're on the server side and have the service key + supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey); +} else if (typeof window === 'undefined' && !supabaseServiceKey) { + // We're on the server side but missing the service key + console.error("Missing SUPABASE_SERVICE_ROLE_KEY environment variable on server side"); +} + +// Export a function to get the admin client, ensuring it's only used server-side +export const getSupabaseAdmin = () => { + if (typeof window !== 'undefined') { + throw new Error("Supabase admin client cannot be used on the client side"); + } + + if (!supabaseAdmin) { + throw new Error("Supabase admin client not initialized. Check SUPABASE_SERVICE_ROLE_KEY environment variable."); + } + + return supabaseAdmin; +}; + +// For backward compatibility, export the admin client directly (but it will be null on client side) +export { supabaseAdmin }; diff --git a/website-monitoring-frontend/src/lib/chart.ts b/website-monitoring-frontend/src/lib/chart.ts new file mode 100644 index 0000000..8fffbc9 --- /dev/null +++ b/website-monitoring-frontend/src/lib/chart.ts @@ -0,0 +1,8 @@ +"use client"; + +import { Chart as ChartJS, registerables } from "chart.js"; + +// Register all Chart.js components once, globally +ChartJS.register(...registerables); + +export { ChartJS }; diff --git a/website-monitoring-frontend/src/lib/supabase.ts b/website-monitoring-frontend/src/lib/supabase.ts new file mode 100644 index 0000000..99afaa0 --- /dev/null +++ b/website-monitoring-frontend/src/lib/supabase.ts @@ -0,0 +1,10 @@ +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; +const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; + +if (!supabaseUrl || !supabaseKey) { + throw new Error("Missing Supabase environment variables"); +} + +export const supabase = createClient(supabaseUrl, supabaseKey); diff --git a/website-monitoring-frontend/src/lib/utils.ts b/website-monitoring-frontend/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/website-monitoring-frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/website-monitoring-frontend/src/middleware.ts b/website-monitoring-frontend/src/middleware.ts new file mode 100644 index 0000000..9cc2ae5 --- /dev/null +++ b/website-monitoring-frontend/src/middleware.ts @@ -0,0 +1,53 @@ +import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export async function middleware(req: NextRequest) { + const res = NextResponse.next(); + const supabase = createMiddlewareClient({ req, res }); + + try { + const { + data: { session }, + } = await supabase.auth.getSession(); + + // Add session user to response headers for client use + if (session?.user) { + // Check if user has an organization + const { data: userData, error: userError } = await supabase + .from("users") + .select("organization_id") + .eq("id", session.user.id) + .single(); + + if (!userError && userData) { + // If user has no organization but is on a page that requires it + if ( + !userData.organization_id && + req.nextUrl.pathname.includes("/dashboard/websites") + ) { + // Redirect to organization creation + return NextResponse.redirect( + new URL("/dashboard/organizations/new", req.url), + ); + } + + if (userData.organization_id) { + res.headers.set("X-Organization-Id", userData.organization_id); + } + } + + if (session.user.user_metadata.role) { + res.headers.set("X-User-Role", session.user.user_metadata.role); + } + } + } catch (error) { + console.error("Middleware error:", error); + } + + return res; +} + +export const config = { + matcher: ["/dashboard/:path*", "/auth/:path*"], +}; diff --git a/website-monitoring-frontend/src/services/crawlManager.ts b/website-monitoring-frontend/src/services/crawlManager.ts new file mode 100644 index 0000000..4295309 --- /dev/null +++ b/website-monitoring-frontend/src/services/crawlManager.ts @@ -0,0 +1,190 @@ +import { useState, useEffect } from "react"; +import { supabase } from "@/lib/supabase"; + +interface CrawlStatus { + id: string; + website_id: string; + status: "pending" | "running" | "completed" | "failed" | "cancelled"; + total_urls: number; + processed_urls: number; + progress_percentage: number; + current_url?: string; + started_at: string; + completed_at?: string; + error_message?: string; +} + +interface CrawlProgress { + totalUrls: number; + processedUrls: number; + progressPercentage: number; + currentUrl?: string; + status: "pending" | "running" | "completed" | "failed" | "cancelled"; +} + +// Client-side safe crawl manager (doesn't use admin functions) +export class CrawlManager { + private sessionId: string; + private websiteId: string; + private progressCallback?: (progress: CrawlProgress) => void; + private currentSubscription?: any; // Track current subscription + + constructor(websiteId: string, sessionId: string) { + this.websiteId = websiteId; + this.sessionId = sessionId; + } + + // Set a callback function to receive real-time progress updates + setProgressCallback(callback: (progress: CrawlProgress) => void) { + this.progressCallback = callback; + } + + // Get current crawl status (client-side safe) + async getCrawlStatus(): Promise<CrawlStatus | null> { + try { + const { data, error } = await supabase + .from("crawl_sessions") + .select("*") + .eq("id", this.sessionId) + .single(); + + if (error || !data) { + return null; + } + + return { + id: data.id, + website_id: data.website_id, + status: data.status, + total_urls: data.total_urls || 0, + processed_urls: data.processed_urls || 0, + progress_percentage: data.progress_percentage || 0, + current_url: data.current_url, + started_at: data.started_at, + completed_at: data.completed_at, + error_message: data.error_message, + } as CrawlStatus; + } catch (error) { + console.error("Failed to get crawl status:", error); + return null; + } + } + + // Subscribe to real-time crawl updates + subscribeToProgress(callback: (progress: CrawlProgress) => void) { + this.setProgressCallback(callback); + + // Unsubscribe from any existing subscription + if (this.currentSubscription) { + this.currentSubscription.unsubscribe(); + } + + const subscription = supabase + .channel(`crawl_session_${this.sessionId}`) + .on( + "postgres_changes", + { + event: "UPDATE", + schema: "public", + table: "crawl_sessions", + filter: `id=eq.${this.sessionId}`, + }, + (payload) => { + const data = payload.new as any; + callback({ + totalUrls: data.total_urls || 0, + processedUrls: data.processed_urls || 0, + progressPercentage: data.progress_percentage || 0, + currentUrl: data.current_url, + status: data.status, + }); + } + ) + .subscribe(); + + this.currentSubscription = subscription; + + return () => { + if (this.currentSubscription) { + this.currentSubscription.unsubscribe(); + this.currentSubscription = undefined; + } + }; + } + + private updateProgress(progress: CrawlProgress) { + if (this.progressCallback) { + this.progressCallback(progress); + } + } +} + +// Factory function to create crawl manager instances +export function createCrawlManager(websiteId: string, sessionId: string): CrawlManager { + return new CrawlManager(websiteId, sessionId); +} + +// Hook for React components to use crawl progress +export function useCrawlProgress(websiteId: string, sessionId?: string) { + const [progress, setProgress] = useState<CrawlProgress>({ + totalUrls: 0, + processedUrls: 0, + progressPercentage: 0, + status: "pending", + }); + + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (!sessionId) { + setIsLoading(false); + return; + } + + let unsubscribe: (() => void) | undefined; + let isSubscribed = true; // Flag to prevent state updates after unmount + + const initProgress = async () => { + try { + const manager = createCrawlManager(websiteId, sessionId); + const status = await manager.getCrawlStatus(); + + if (status && isSubscribed) { + setProgress({ + totalUrls: status.total_urls, + processedUrls: status.processed_urls, + progressPercentage: status.progress_percentage, + currentUrl: status.current_url, + status: status.status, + }); + + // Subscribe to real-time updates + unsubscribe = manager.subscribeToProgress((newProgress) => { + if (isSubscribed) { + setProgress(newProgress); + } + }); + } + } catch (error) { + if (isSubscribed) { + console.error("Failed to initialize crawl progress:", error); + } + } finally { + if (isSubscribed) { + setIsLoading(false); + } + } + }; + + initProgress(); + + return () => { + isSubscribed = false; // Prevent state updates after unmount + if (unsubscribe) { + unsubscribe(); + } + }; + }, [websiteId, sessionId]); // Keep sessionId in dependencies but handle cleanup properly + + return { progress, isLoading }; +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/services/crawlerService.ts b/website-monitoring-frontend/src/services/crawlerService.ts new file mode 100644 index 0000000..dc52479 --- /dev/null +++ b/website-monitoring-frontend/src/services/crawlerService.ts @@ -0,0 +1,717 @@ +import { supabase } from "@/lib/supabase"; +import { getSupabaseAdmin } from "@/lib/admin"; +import { createServerCrawlManager } from "./serverCrawlManager"; +import { JSDOM } from "jsdom"; +import { URL } from "url"; +import puppeteer from "puppeteer"; +import robotsParser from "robots-parser"; + +interface CrawlQueueItem { + id: string; + website_id: string; + url: string; + discovery_depth: number; + attempts?: number; + parent_url?: string | null; + status: string; + priority: number; + created_at?: string; + updated_at?: string; +} + +interface PageMapping { + [url: string]: string; // Maps URL to page ID +} + +export class CrawlerService { + private websiteId: string; + private baseUrl: string = ""; + private maxDepth: number = 3; + private maxPages: number = 100; + private robotsTxt: any = null; + private respectRobotsTxt: boolean = false; + private sessionId: string; + private excludePatterns: string[] = []; + private includePatterns: string[] = ["/*"]; + private urlPatterns = new Map<string, number>(); + private pageIdMapping: PageMapping = {}; // Track URL to page ID mapping + private discoveredUrls: string[] = []; // Track discovered URLs for this session + + constructor(websiteId: string, sessionId: string) { + this.websiteId = String(websiteId).trim().toLowerCase(); + this.sessionId = String(sessionId).trim().toLowerCase(); + console.log( + `CrawlerService initialized with websiteId: ${this.websiteId}, sessionId: ${this.sessionId}`, + ); + } + + async initialize() { + // Get website configuration + const { data: website } = await getSupabaseAdmin() + .from("websites") + .select("base_url, crawl_settings") + .eq("id", this.websiteId) + .single(); + + if (!website) throw new Error("Website not found"); + + // Canonicalize baseUrl by following redirects (e.g. non-www -> www) + let canonicalBaseUrl = website.base_url as string; + try { + const res = await fetch(website.base_url as string, { method: 'HEAD', redirect: 'follow' }); + if (res.url) { + canonicalBaseUrl = res.url; + console.log(`[initialize] Canonical base URL detected: ${canonicalBaseUrl}`); + } + } catch (e) { + console.log(`[initialize] Could not resolve canonical base URL, using as is: ${website.base_url}`); + } + this.baseUrl = canonicalBaseUrl; + this.maxDepth = (website.crawl_settings as any)?.max_depth || 3; + this.maxPages = (website.crawl_settings as any)?.max_pages || 100; + this.excludePatterns = (website.crawl_settings as any)?.exclude_patterns || []; + this.includePatterns = (website.crawl_settings as any)?.include_patterns || ["/*"]; + this.respectRobotsTxt = (website.crawl_settings as any)?.respect_robots_txt || false; + + // Reset local tracking + this.urlPatterns.clear(); + this.pageIdMapping = {}; + this.discoveredUrls = []; + this.robotsTxt = null; + + // Fetch and parse robots.txt if needed + if (this.respectRobotsTxt) { + try { + const robotsUrl = new URL("/robots.txt", this.baseUrl).href; + const res = await fetch(robotsUrl, { headers: { "User-Agent": "CloudLense Crawler" } }); + if (res.ok) { + const robotsTxtBody = await res.text(); + this.robotsTxt = robotsParser(robotsUrl, robotsTxtBody); + console.log("robots.txt loaded and parsed"); + } else { + console.log("robots.txt not found or not accessible"); + } + } catch (e) { + console.log("Failed to fetch robots.txt:", e); + } + } + + // Create crawl session + await getSupabaseAdmin() + .from("crawl_sessions") + .update({ + pages_discovered: 0, + pages_processed: 0, + status: "running", + }) + .eq("id", this.sessionId); + } + + async crawl() { + try { + await this.initialize(); + console.log(`Starting crawl for website: ${this.websiteId}`); + + // Clear existing queue items + await getSupabaseAdmin() + .from("crawl_queue") + .delete() + .eq("website_id", this.websiteId); + + // Add the starting URL + await this.addToCrawlQueue(this.baseUrl, 0, null); + + // Process queue items until empty + let hasMore = true; + while (hasMore) { + const { data: queueItems } = await getSupabaseAdmin() + .from("crawl_queue") + .select("*") + .eq("website_id", this.websiteId) + .eq("status", "pending") + .order("priority", { ascending: false }) + .limit(10); // Process in batches + + if (!queueItems || queueItems.length === 0) { + hasMore = false; + continue; + } + + // Process each item one at a time to maintain proper parent-child relationships + for (const item of queueItems) { + await this.processUrl(item as any); + + // Update the crawl session stats periodically to show progress + await this.updateSessionStats(); + } + } + + // Store all discovered URLs in the crawl_sessions metadata + await getSupabaseAdmin() + .from("crawl_sessions") + .update({ + metadata: { discovered_urls: this.discoveredUrls }, + }) + .eq("id", this.sessionId); + + await this.finishSession(); + console.log(`Crawl completed for website: ${this.websiteId}`); + } catch (error: unknown) { + console.error("Crawl error:", error); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + await this.updateSessionStatus("failed", errorMessage); + } + } + + private async processUrl(queueItem: CrawlQueueItem) { + console.log(`[processUrl] discovery_depth: ${queueItem.discovery_depth}, maxDepth: ${this.maxDepth}`); + let statusSet = false; + // Variablen für den gesamten Scope deklarieren + let html: string = ""; + let title: string = ""; + let description: string = ""; + let links: string[] = []; + let contentHash: string = ""; + let urlObj: URL; + let parentPageId = queueItem.parent_url + ? this.pageIdMapping[this.normalizeUrl(queueItem.parent_url)] || null + : null; + try { + const normalizedUrl = this.normalizeUrl(queueItem.url); + urlObj = new URL(queueItem.url); + // Check local set first to avoid duplicate processing + const { data: existingPage, error: existingPageError } = await getSupabaseAdmin() + .from("pages") + .select("id") + .eq("website_id", this.websiteId) + .eq("url", normalizedUrl) + .limit(1); + if (!existingPageError && existingPage && existingPage.length > 0) { + // Update bestehende Seite + await getSupabaseAdmin() + .from("pages") + .update({ + last_crawled_at: new Date().toISOString(), + metadata: { crawl_session_id: this.sessionId } as any, + }) + .eq("id", (existingPage[0] as any).id); + } else { + // Werte wie bisher extrahieren + if (queueItem.discovery_depth === 0) { + const browser = await puppeteer.launch({ headless: true }); + const page = await browser.newPage(); + await page.goto(queueItem.url, { waitUntil: "networkidle2" }); + html = await page.content(); + links = await page.$$eval("a[href]", (as) => + as.map((a) => (a as HTMLAnchorElement).href), + ); + title = await page.title(); + description = + (await page + .$eval('meta[name="description"]', (el) => + el.getAttribute("content"), + ) + .catch(() => "")) || ""; + await browser.close(); + } else { + const response = await fetch(queueItem.url, { + redirect: "follow", + headers: { "User-Agent": "CloudLense Crawler" }, + }); + html = await response.text(); + const dom = new JSDOM(html); + const document = dom.window.document; + title = document.title; + description = + document + .querySelector('meta[name="description"]') + ?.getAttribute("content") || ""; + links = this.extractLinks(document, queueItem.url); + } + contentHash = await this.computeContentHash(html); + await getSupabaseAdmin() + .from("pages") + .insert([ + { + website_id: this.websiteId, + url: normalizedUrl, + path: urlObj.pathname, + title, + description, + content_hash: contentHash, + content_type: "text/html", + status_code: 200, + depth: queueItem.discovery_depth, + parent_page_id: parentPageId, + is_active: true, + last_crawled_at: new Date().toISOString(), + metadata: { + crawl_session_id: this.sessionId, + outbound_links: links.length, + }, + }, + ]); + } + + // Mark URL as processed + this.discoveredUrls.push(normalizedUrl); + this.updateUrlPatterns(queueItem.url); + + const urlWithoutQuery = urlObj.origin + urlObj.pathname; + + // Track this URL for this crawl session + this.discoveredUrls.push(urlWithoutQuery); + + // Determine parent page ID - use the known page ID mapping or null + parentPageId = queueItem.parent_url + ? this.pageIdMapping[this.normalizeUrl(queueItem.parent_url)] || null + : null; + + // Check for homepage duplicate content before saving + let isHomepage = false; + try { + const normalizedBaseUrl = this.baseUrl.replace(/\/$/, ""); + const normalizedCurrentUrl = urlWithoutQuery.replace(/\/$/, ""); + isHomepage = normalizedCurrentUrl === normalizedBaseUrl; + } catch (e) { + // fallback: treat as not homepage + } + + if (!isHomepage) { + // Fetch homepage content_hash from DB + const { data: homepagePage, error: homepageError } = await getSupabaseAdmin() + .from("pages") + .select("content_hash") + .eq("website_id", this.websiteId) + .eq("url", this.baseUrl.replace(/\/$/, "")) + .single(); + + if ( + homepagePage && + homepagePage.content_hash && + homepagePage.content_hash === contentHash + ) { + console.log( + `Skipping ${urlWithoutQuery} (duplicate content of homepage)`, + ); + // Mark as skipped in crawl_queue + await getSupabaseAdmin() + .from("crawl_queue") + .update({ + status: "skipped", + error_message: "Duplicate content of homepage", + }) + .eq("id", queueItem.id); + statusSet = true; + // Session stats will be updated by updateSessionStats() + return; + } + } + + // Now save the page - all pages are active by default + // Prevent duplicate page insertion + const { data: existingPages, error: existingError } = await getSupabaseAdmin() + .from("pages") + .select("id") + .eq("website_id", this.websiteId) + .eq("url", urlWithoutQuery) + .limit(1); + if (!existingError && existingPages && existingPages.length > 0) { + // Page already exists, skip insert + return; + } + const { data: pageDb, error: pageInsertError } = await getSupabaseAdmin() + .from("pages") + .insert([ + { + website_id: this.websiteId, + url: urlWithoutQuery, + path: urlObj.pathname, + title, + description, + content_hash: contentHash, + content_type: "text/html", + status_code: 200, + depth: queueItem.discovery_depth, + parent_page_id: parentPageId, + is_active: true, // Default to active, user can deselect later + metadata: { + crawl_session_id: this.sessionId, // Store session ID in metadata + outbound_links: links.length, + }, + }, + ]) + .select() + .single(); + + if (pageInsertError) { + console.error( + `Could not insert page ${normalizedUrl}: ${pageInsertError.message}`, + ); + + // Still mark as completed to avoid endless retries + await getSupabaseAdmin() + .from("crawl_queue") + .update({ + status: "failed", + error_message: pageInsertError.message, + }) + .eq("id", queueItem.id); + statusSet = true; + + // Session stats will be updated by updateSessionStats() + return; + } + + // Save the page ID mapping for future reference + if (pageDb) { + this.pageIdMapping[normalizedUrl] = (pageDb as any).id as string; + + console.log( + `Processed URL: ${queueItem.url}, Title: ${title}`, + `Links found: ${links.length}, Depth: ${queueItem.discovery_depth}`, + ); + + // Mark as completed + await getSupabaseAdmin() + .from("crawl_queue") + .update({ status: "completed" }) + .eq("id", queueItem.id); + statusSet = true; + + // Session stats will be updated by updateSessionStats() + + // Schedule a scan for this page + await this.scheduleLighthouseScan(pageDb.id as string); + + // NOW that we have saved the page and have a valid ID, + // we can queue child pages so they'll have a valid parent reference + if (queueItem.discovery_depth < this.maxDepth) { + console.log(`[processUrl] Will queue child URLs for ${queueItem.url}`); + await this.queueNewUrls(links, queueItem.discovery_depth + 1, queueItem.url); + } + } + } catch (error: unknown) { + console.error(`Failed to process URL ${queueItem.url}:`, error); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + await getSupabaseAdmin() + .from("crawl_queue") + .update({ + status: "failed", + error_message: errorMessage, + attempts: (queueItem.attempts || 0) + 1, + }) + .eq("id", queueItem.id); + statusSet = true; + + // Session stats will be updated by updateSessionStats() + } finally { + // Fallback: falls Status nicht gesetzt wurde, auf "failed" setzen + if (!statusSet) { + await getSupabaseAdmin() + .from("crawl_queue") + .update({ + status: "failed", + error_message: "Unknown error (finally block)", + }) + .eq("id", queueItem.id); + } + } + } + + // Update session stats directly with our local counters + private async updateSessionStats() { + const { count: discoveredCount } = await getSupabaseAdmin() + .from("crawl_queue") + .select("id", { count: "exact", head: true }) + .eq("website_id", this.websiteId); + const { count: processedCount } = await getSupabaseAdmin() + .from("crawl_queue") + .select("id", { count: "exact", head: true }) + .eq("website_id", this.websiteId) + .in("status", ["completed", "failed", "skipped"]); + await getSupabaseAdmin() + .from("crawl_sessions") + .update({ + pages_discovered: discoveredCount || 0, + pages_processed: processedCount || 0, + }) + .eq("id", this.sessionId); + } + + // Normalize URL to ensure consistent comparison + private normalizeUrl(url: string): string { + try { + const u = new URL(url); + // Always use the canonical host (from this.baseUrl) + const canonicalHost = new URL(this.baseUrl).host; + u.host = canonicalHost; + // Remove query and hash + u.search = ''; + u.hash = ''; + // Remove trailing slash for all except root + if (u.pathname !== '/') { + u.pathname = u.pathname.replace(/\/$/, ''); + } + return u.origin + u.pathname; + } catch { + return url; + } + } + + private extractLinks(document: Document, baseUrl: string): string[] { + try { + const links = Array.from(document.querySelectorAll("a[href]")) + .map((link) => { + try { + // Normalize the URL + const href = link.getAttribute("href"); + if (!href) return null; + + // Handle relative URLs + const url = new URL(href, baseUrl); + return url.href; + } catch { + return null; + } + }) + .filter((url): url is string => { + if (!url) return false; + try { + const baseHost = new URL(this.baseUrl).host.replace(/^www\./, ""); + const urlHost = new URL(url).host.replace(/^www\./, ""); + return baseHost === urlHost; + } catch { + return false; + } + }) + .filter((url) => { + const path = new URL(url).pathname; + + // Check include patterns first + const shouldInclude = this.includePatterns.some((pattern) => + this.matchPattern(path, pattern), + ); + + // Then check exclude patterns + const shouldExclude = this.excludePatterns.some((pattern) => + this.matchPattern(path, pattern), + ); + + if (!shouldInclude) { + console.log(`Filtered out by includePatterns: ${url}`); + } + if (shouldExclude) { + console.log(`Filtered out by excludePatterns: ${url}`); + } + + return shouldInclude && !shouldExclude; + }); + + const uniqueLinks = Array.from(new Set(links)); // Remove duplicates + console.log(`Extracted ${uniqueLinks.length} unique links from page ${baseUrl}`); + return uniqueLinks; + } catch (error: unknown) { + console.error("Error extracting links:", error); + return []; + } + } + + private matchPattern(path: string, pattern: string): boolean { + // Convert wildcard pattern to regex + const regexPattern = pattern.replace(/\*/g, ".*"); + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(path); + } + + private detectUrlPattern(url: string): string | null { + try { + const urlObj = new URL(url); + const path = urlObj.pathname; + + // Check for common patterns in e-commerce and content sites + const patterns = [ + /\/products?\/([^\/]+)/i, // Product pages + /\/categories?\/([^\/]+)/i, // Category pages + /\/articles?\/([^\/]+)/i, // Article pages + /\/posts?\/([^\/]+)/i, // Blog posts + /\/users?\/([^\/]+)/i, // User pages + /\/tags?\/([^\/]+)/i, // Tag pages + /\/([0-9]+)\/([^\/]+)/, // ID-based pages + ]; + + for (const regex of patterns) { + if (regex.test(path)) { + return regex.toString(); + } + } + + return null; + } catch { + return null; + } + } + + private updateUrlPatterns(url: string): void { + const pattern = this.detectUrlPattern(url); + if (pattern) { + const count = this.urlPatterns.get(pattern) || 0; + this.urlPatterns.set(pattern, count + 1); + } + } + + private async addToCrawlQueue( + url: string, + depth: number, + parentUrl: string | null, + ) { + // First normalize the URL to ensure consistent comparison + const normalizedUrl = this.normalizeUrl(url); + console.log(`[addToCrawlQueue] Called with:`, { url, normalizedUrl, depth, parentUrl }); + + // Max pages limit + if (this.discoveredUrls.length >= this.maxPages) { + console.log(`[addToCrawlQueue] Max pages (${this.maxPages}) reached, not queuing: ${normalizedUrl}`); + return; + } + + // Prüfe, ob die URL schon in der aktuellen crawl_queue für diese Session steht + const { data: existingQueue, error: existingQueueError } = await getSupabaseAdmin() + .from("crawl_queue") + .select("id") + .eq("website_id", this.websiteId) + .eq("url", normalizedUrl) + .eq("status", "pending") + .limit(1); + if (!existingQueueError && existingQueue && existingQueue.length > 0) { + // Bereits in der Queue für diese Session + return; + } + + // robots.txt check + if (this.robotsTxt && !this.robotsTxt.isAllowed(normalizedUrl, "CloudLense Crawler")) { + console.log(`[addToCrawlQueue] Blocked by robots.txt: ${normalizedUrl}`); + return; + } + + // only insert if in base_url domain + const baseUrlObj = new URL(this.baseUrl); + try { + const urlObj = new URL(url); + // Accept both www and non-www as equivalent + const normalizeHost = (host: string) => host.replace(/^www\./, "").toLowerCase(); + const baseHost = normalizeHost(baseUrlObj.host); + const urlHost = normalizeHost(urlObj.host); + if (baseHost !== urlHost) { + console.log(`[addToCrawlQueue] Domain mismatch: baseHost=${baseHost}, urlHost=${urlHost} for url=${url}`); + return; + } + } catch (error) { + console.error(`[addToCrawlQueue] Invalid URL: ${url}`, error); + return; + } + + // Add to local tracking before DB query to prevent race conditions + // This set is no longer needed as we check directly against DB + + console.log(`[addToCrawlQueue] Queuing: ${normalizedUrl} at depth ${depth}`); + + // Add to database queue + const { error: insertError } = await getSupabaseAdmin().from("crawl_queue").insert([ + { + website_id: this.websiteId, + url: normalizedUrl, + discovery_depth: depth, + parent_url: parentUrl, + status: "pending", + priority: depth === 0 ? 10 : 5, + } as any, + ]); + if (insertError) { + console.error(`[addToCrawlQueue] Failed to insert ${normalizedUrl}:`, insertError.message); + return; + } + + // Increment our local counter + // This counter is no longer needed as we count directly from DB + console.log(`[addToCrawlQueue] Queued: ${normalizedUrl} (pagesDiscovered: ${this.discoveredUrls.length})`); + } + + private async queueNewUrls( + urls: string[], + depth: number, + parentUrl: string, + ) { + console.log(`[queueNewUrls] Called with ${urls.length} URLs at depth ${depth} (maxDepth: ${this.maxDepth})`); + console.log(`[queueNewUrls] URLs:`, urls); + // Filter out URLs that are already being tracked locally + for (const url of urls) { + const normalizedUrl = this.normalizeUrl(url); + const { data: existingChild, error: existingChildError } = await getSupabaseAdmin() + .from("pages") + .select("id") + .eq("website_id", this.websiteId) + .eq("url", normalizedUrl) + .limit(1); + if (!existingChildError && existingChild && existingChild.length > 0) { + console.log(`[queueNewUrls] Filtered out: ${url} (normalized: ${normalizedUrl}) | Exists: true`); + continue; + } + await this.addToCrawlQueue(url, depth, parentUrl); + } + } + + private async markUrlSkipped(queueItemId: string) { + await getSupabaseAdmin() + .from("crawl_queue") + .update({ + status: "skipped", + error_message: "URL already processed", + }) + .eq("id", queueItemId); + } + + private async scheduleLighthouseScan(pageId: string) { + await getSupabaseAdmin().from("scans").insert([ + { + website_id: this.websiteId, + page_id: pageId, + status: "pending", + }, + ]); + } + + private async finishSession() { + // Final update of session stats + await this.updateSessionStats(); + await this.updateSessionStatus("completed"); + } + + private async updateSessionStatus(status: string, error?: string) { + await getSupabaseAdmin() + .from("crawl_sessions") + .update({ + status, + error_message: error, + completed_at: new Date().toISOString(), + }) + .eq("id", this.sessionId); + } + + private countWords(html: string): number { + // Extract text content and count words + const textContent = html.replace(/<[^>]*>/g, " "); + return textContent.split(/\s+/).filter(Boolean).length; + } + + private async computeContentHash(content: string): Promise<string> { + // Simple hash for content fingerprinting + const encoder = new TextEncoder(); + const data = encoder.encode(content); + const hashBuffer = await crypto.subtle.digest("SHA-1", data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); + } +} diff --git a/website-monitoring-frontend/src/services/lighthouseScanner.ts b/website-monitoring-frontend/src/services/lighthouseScanner.ts new file mode 100644 index 0000000..c8f0824 --- /dev/null +++ b/website-monitoring-frontend/src/services/lighthouseScanner.ts @@ -0,0 +1,425 @@ +import { getSupabaseAdmin } from "@/lib/admin"; +import { logError } from "@/utils/errorUtils"; + +export interface ScanConfig { + websiteId: string; + pageId: string; + deviceType: 'desktop' | 'mobile'; + categories: ('performance' | 'accessibility' | 'seo' | 'best_practices')[]; + priority: 'low' | 'medium' | 'high'; + triggeredBy?: 'change_detection' | 'scheduled' | 'manual'; +} + +export interface ScanResult { + scanId: string; + success: boolean; + error?: string; + metrics?: { + performance: number; + accessibility: number; + seo: number; + bestPractices: number; + firstContentfulPaint: number; + largestContentfulPaint: number; + totalBlockingTime: number; + cumulativeLayoutShift: number; + }; + rawData?: any; +} + +export interface SubscriptionLimits { + maxScansPerDay: number; + maxScansPerMonth: number; + changeDetectionEnabled: boolean; + scheduledScansEnabled: boolean; + scanFrequency: 'hourly' | 'daily' | 'weekly' | 'monthly'; +} + +export class LighthouseScanner { + private supabase = getSupabaseAdmin(); + + /** + * Check if a website has changes by comparing content hashes + */ + async detectChanges(websiteId: string): Promise<boolean> { + try { + // Get the latest page for this website + const { data: latestPage, error: pageError } = await this.supabase + .from('pages') + .select('id, url, content_hash, last_seen_at') + .eq('website_id', websiteId) + .eq('is_active', true) + .order('last_seen_at', { ascending: false }) + .limit(1) + .single(); + + if (pageError || !latestPage) { + return false; + } + + // Fetch the current page content + const response = await fetch(latestPage.url as string, { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; WebsiteMonitor/1.0)', + }, + }); + + if (!response.ok) { + return false; + } + + const content = await response.text(); + const currentHash = await this.computeContentHash(content); + + // If content hash has changed, there are changes + return currentHash !== latestPage.content_hash; + } catch (error) { + logError('Error detecting changes', error, { websiteId }); + return false; + } + } + + /** + * Check subscription limits before allowing a scan + */ + async checkSubscriptionLimits(organizationId: string): Promise<{ + canScan: boolean; + limits: SubscriptionLimits; + currentUsage: { daily: number; monthly: number }; + }> { + try { + // Get organization subscription info + const { data: org, error: orgError } = await this.supabase + .from('organizations') + .select('subscription_tier, settings') + .eq('id', organizationId) + .single(); + + if (orgError || !org) { + return { + canScan: false, + limits: this.getDefaultLimits(), + currentUsage: { daily: 0, monthly: 0 }, + }; + } + + // Get subscription limits based on tier + const limits = this.getSubscriptionLimits(org.subscription_tier as string); + + // Get current scan usage + const now = new Date(); + const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + const { data: dailyScans } = await this.supabase + .from('scans') + .select('id') + .eq('website_id', organizationId) + .gte('created_at', startOfDay.toISOString()); + + const { data: monthlyScans } = await this.supabase + .from('scans') + .select('id') + .eq('website_id', organizationId) + .gte('created_at', startOfMonth.toISOString()); + + const currentUsage = { + daily: dailyScans?.length || 0, + monthly: monthlyScans?.length || 0, + }; + + const canScan = currentUsage.daily < limits.maxScansPerDay && + currentUsage.monthly < limits.maxScansPerMonth; + + return { canScan, limits, currentUsage }; + } catch (error) { + logError('Error checking subscription limits', error, { organizationId }); + return { + canScan: false, + limits: this.getDefaultLimits(), + currentUsage: { daily: 0, monthly: 0 }, + }; + } + } + + /** + * Perform a Lighthouse scan + */ + async performScan(config: ScanConfig): Promise<ScanResult> { + try { + // Create scan record + const { data: scan, error: scanError } = await this.supabase + .from('scans') + .insert({ + website_id: config.websiteId, + page_id: config.pageId, + scan_type: 'full', + status: 'running', + priority: config.priority === 'high' ? 1 : config.priority === 'medium' ? 2 : 3, + categories: config.categories, + device_type: config.deviceType, + started_at: new Date().toISOString(), + }) + .select() + .single(); + + if (scanError || !scan) { + throw new Error(`Failed to create scan record: ${scanError?.message}`); + } + + // Get page URL + const { data: page, error: pageError } = await this.supabase + .from('pages') + .select('url') + .eq('id', config.pageId) + .single(); + + if (pageError || !page) { + throw new Error(`Page not found: ${pageError?.message}`); + } + + // Perform Lighthouse scan + const lighthouseResult = await this.runLighthouse(page.url as string, config.deviceType); + + // Save scan results + await this.saveScanResults(scan.id as string, lighthouseResult); + + // Update scan status + await this.supabase + .from('scans') + .update({ + status: 'completed', + completed_at: new Date().toISOString(), + duration_ms: Date.now() - new Date(scan.started_at as string).getTime(), + }) + .eq('id', scan.id as string); + + return { + scanId: scan.id as string, + success: true, + metrics: lighthouseResult.metrics, + rawData: lighthouseResult.rawData, + }; + } catch (error) { + logError('Error performing Lighthouse scan', error, { config }); + + // Update scan status to failed + if (config.websiteId && config.pageId) { + await this.supabase + .from('scans') + .update({ + status: 'failed', + error_message: error instanceof Error ? error.message : 'Unknown error', + completed_at: new Date().toISOString(), + }) + .eq('website_id', config.websiteId) + .eq('page_id', config.pageId) + .eq('status', 'running'); + } + + return { + scanId: '', + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + /** + * Run Lighthouse scan using the scanner worker + */ + private async runLighthouse(url: string, deviceType: 'desktop' | 'mobile'): Promise<{ + metrics: any; + rawData: any; + }> { + try { + const lighthouseUrl = process.env.LIGHTHOUSE_SERVICE_URL || 'http://localhost:5001'; + const response = await fetch(`${lighthouseUrl}/lighthouse`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ url }), + }); + + if (!response.ok) { + throw new Error(`Lighthouse service responded with ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + + // Extract metrics from the Lighthouse result + const metrics = { + performance: result.lhr?.categories?.performance?.score * 100 || 0, + accessibility: result.lhr?.categories?.accessibility?.score * 100 || 0, + seo: result.lhr?.categories?.seo?.score * 100 || 0, + bestPractices: result.lhr?.categories?.['best-practices']?.score * 100 || 0, + firstContentfulPaint: result.lhr?.audits?.['first-contentful-paint']?.numericValue || 0, + largestContentfulPaint: result.lhr?.audits?.['largest-contentful-paint']?.numericValue || 0, + totalBlockingTime: result.lhr?.audits?.['total-blocking-time']?.numericValue || 0, + cumulativeLayoutShift: result.lhr?.audits?.['cumulative-layout-shift']?.numericValue || 0, + }; + + return { + metrics, + rawData: result.lhr || result, + }; + } catch (error) { + console.error('Lighthouse scan failed:', error); + throw error; + } + } + + /** + * Save scan results to database + */ + private async saveScanResults(scanId: string, result: any): Promise<void> { + const { metrics, rawData } = result; + + // Save scan results by category + const categories = ['performance', 'accessibility', 'seo', 'best_practices']; + for (const category of categories) { + await this.supabase + .from('scan_results') + .insert({ + scan_id: scanId, + category, + score: metrics[category], + raw_data: rawData, + }); + } + + // Save metric values + const metricMappings = [ + { key: 'performance', value: metrics.performance }, + { key: 'accessibility', value: metrics.accessibility }, + { key: 'seo', value: metrics.seo }, + { key: 'bestPractices', value: metrics.bestPractices }, + { key: 'firstContentfulPaint', value: metrics.firstContentfulPaint }, + { key: 'largestContentfulPaint', value: metrics.largestContentfulPaint }, + { key: 'totalBlockingTime', value: metrics.totalBlockingTime }, + { key: 'cumulativeLayoutShift', value: metrics.cumulativeLayoutShift }, + ]; + + for (const mapping of metricMappings) { + const { data: metricDef } = await this.supabase + .from('metric_definitions') + .select('id') + .eq('key', mapping.key) + .single(); + + if (metricDef) { + await this.supabase + .from('metric_values') + .insert({ + scan_id: scanId, + metric_id: metricDef.id, + value: mapping.value, + unit: this.getMetricUnit(mapping.key), + is_passing: this.isMetricPassing(mapping.key, mapping.value), + }); + } + } + } + + /** + * Get subscription limits based on tier + */ + private getSubscriptionLimits(tier: string): SubscriptionLimits { + switch (tier) { + case 'free': + return { + maxScansPerDay: 5, + maxScansPerMonth: 50, + changeDetectionEnabled: false, + scheduledScansEnabled: false, + scanFrequency: 'weekly', + }; + case 'starter': + return { + maxScansPerDay: 20, + maxScansPerMonth: 200, + changeDetectionEnabled: true, + scheduledScansEnabled: true, + scanFrequency: 'daily', + }; + case 'professional': + return { + maxScansPerDay: 100, + maxScansPerMonth: 1000, + changeDetectionEnabled: true, + scheduledScansEnabled: true, + scanFrequency: 'hourly', + }; + case 'enterprise': + return { + maxScansPerDay: 500, + maxScansPerMonth: 5000, + changeDetectionEnabled: true, + scheduledScansEnabled: true, + scanFrequency: 'hourly', + }; + default: + return this.getDefaultLimits(); + } + } + + private getDefaultLimits(): SubscriptionLimits { + return { + maxScansPerDay: 5, + maxScansPerMonth: 50, + changeDetectionEnabled: false, + scheduledScansEnabled: false, + scanFrequency: 'weekly', + }; + } + + private getMetricUnit(key: string): string { + const units: Record<string, string> = { + performance: '%', + accessibility: '%', + seo: '%', + bestPractices: '%', + firstContentfulPaint: 'ms', + largestContentfulPaint: 'ms', + totalBlockingTime: 'ms', + cumulativeLayoutShift: 'score', + }; + return units[key] || ''; + } + + private isMetricPassing(key: string, value: number): boolean { + const thresholds: Record<string, number> = { + performance: 90, + accessibility: 90, + seo: 90, + bestPractices: 90, + firstContentfulPaint: 1800, + largestContentfulPaint: 2500, + totalBlockingTime: 200, + cumulativeLayoutShift: 0.1, + }; + + const threshold = thresholds[key]; + if (!threshold) return true; + + // For metrics where lower is better + const lowerIsBetter = ['firstContentfulPaint', 'largestContentfulPaint', 'totalBlockingTime', 'cumulativeLayoutShift']; + if (lowerIsBetter.includes(key)) { + return value <= threshold; + } + + return value >= threshold; + } + + private async computeContentHash(content: string): Promise<string> { + const encoder = new TextEncoder(); + const data = encoder.encode(content); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + } +} + +export const lighthouseScanner = new LighthouseScanner(); \ No newline at end of file diff --git a/website-monitoring-frontend/src/services/monitoringService.ts b/website-monitoring-frontend/src/services/monitoringService.ts new file mode 100644 index 0000000..a5a3459 --- /dev/null +++ b/website-monitoring-frontend/src/services/monitoringService.ts @@ -0,0 +1,96 @@ +import { supabase } from "@/lib/supabase"; + +export const monitoringService = { + async addWebsite(organizationId: string, url: string, name: string) { + const { data: website, error } = await supabase + .from("websites") + .insert([ + { + organization_id: organizationId, + base_url: url, + name: name || new URL(url).hostname, + is_active: true, + }, + ]) + .select() + .single(); + + if (error) throw error; + return website; + }, + + async initiateAnalysis(websiteId: string) { + // First, add the root URL to crawl queue + const { data: website } = await supabase + .from("websites") + .select("base_url") + .eq("id", websiteId) + .single(); + + if (!website) throw new Error("Website not found"); + + // Create crawl session + const { data: session, error: sessionError } = await supabase + .from("crawl_sessions") + .insert([ + { + website_id: websiteId, + start_url: website.base_url, + status: "running", + }, + ]) + .select() + .single(); + + if (sessionError) throw sessionError; + + // Add root URL to crawl queue + const { error: queueError } = await supabase.from("crawl_queue").insert([ + { + website_id: websiteId, + url: website.base_url, + priority: 1, + }, + ]); + + if (queueError) throw queueError; + + // Trigger the analysis (you'll need to implement this API endpoint) + const response = await fetch("/api/analyze", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + websiteId, + sessionId: session.id, + }), + }); + + if (!response.ok) { + throw new Error("Failed to initiate analysis"); + } + + return session; + }, + + async getAnalysisResults(websiteId: string) { + const { data, error } = await supabase + .from("scans") + .select( + ` + *, + scan_results (*), + metric_values ( + *, + metric_definitions (*) + ), + resource_analysis (*) + `, + ) + .eq("website_id", websiteId) + .order("created_at", { ascending: false }) + .limit(1); + + if (error) throw error; + return data?.[0]; + }, +}; diff --git a/website-monitoring-frontend/src/services/newCrawlerService.ts b/website-monitoring-frontend/src/services/newCrawlerService.ts new file mode 100644 index 0000000..68686e5 --- /dev/null +++ b/website-monitoring-frontend/src/services/newCrawlerService.ts @@ -0,0 +1,350 @@ +import { getSupabaseAdmin } from "@/lib/admin"; +import { createServerCrawlManager } from "./serverCrawlManager"; +import { JSDOM } from "jsdom"; +import { URL } from "url"; +import puppeteer from "puppeteer"; + +interface CrawlerConfig { + maxDepth: number; + maxPages: number; + excludePatterns: string[]; + includePatterns: string[]; + respectRobotsTxt: boolean; +} + +export class NewCrawlerService { + private websiteId: string; + private sessionId: string; + private baseUrl: string = ""; + private config: CrawlerConfig = { + maxDepth: 3, + maxPages: 100, + excludePatterns: [], + includePatterns: ["/*"], + respectRobotsTxt: false, + }; + private crawlManager: any; + private urlQueue: Set<string> = new Set(); + private processedUrls: Set<string> = new Set(); + private currentDepth: number = 0; + + constructor(websiteId: string, sessionId: string) { + this.websiteId = websiteId; + this.sessionId = sessionId; + this.crawlManager = createServerCrawlManager(websiteId, sessionId); + } + + async initialize(): Promise<void> { + console.log(`[NewCrawler] Initializing crawl for website: ${this.websiteId}`); + + // Get website configuration + const { data: website, error } = await getSupabaseAdmin() + .from("websites") + .select("base_url, crawl_settings") + .eq("id", this.websiteId) + .single(); + + if (error || !website) { + throw new Error(`Website not found: ${error?.message}`); + } + + this.baseUrl = website.base_url as string; + + // Apply crawl settings + if (website.crawl_settings) { + this.config = { + ...this.config, + ...website.crawl_settings, + }; + } + + // Initialize crawl manager + await this.crawlManager.initializeCrawl(); + + console.log(`[NewCrawler] Initialized with baseUrl: ${this.baseUrl}, config:`, this.config); + } + + async startCrawl(): Promise<void> { + try { + await this.initialize(); + + // Start with the base URL + this.urlQueue.add(this.baseUrl); + + // Update progress: we know we have at least 1 URL to process + await this.crawlManager.updateCrawlProgress(1, 0, this.baseUrl, "running"); + + // Process URLs level by level (breadth-first) + for (let depth = 0; depth <= this.config.maxDepth; depth++) { + this.currentDepth = depth; + await this.processCurrentLevel(); + + if (this.processedUrls.size >= this.config.maxPages) { + console.log(`[NewCrawler] Reached max pages limit: ${this.config.maxPages}`); + break; + } + } + + await this.crawlManager.completeCrawl(true); + console.log(`[NewCrawler] Crawl completed successfully`); + + } catch (error) { + console.error(`[NewCrawler] Crawl failed:`, error); + await this.crawlManager.completeCrawl(false, error instanceof Error ? error.message : "Unknown error"); + throw error; + } + } + + private async processCurrentLevel(): Promise<void> { + const urlsToProcess = Array.from(this.urlQueue); + const nextLevelUrls: Set<string> = new Set(); + + console.log(`[NewCrawler] Processing level ${this.currentDepth} with ${urlsToProcess.length} URLs`); + + for (const url of urlsToProcess) { + if (this.processedUrls.has(url) || this.processedUrls.size >= this.config.maxPages) { + continue; + } + + try { + // Update progress with current URL + await this.crawlManager.updateCrawlProgress( + this.urlQueue.size + this.processedUrls.size, + this.processedUrls.size, + url, + "running" + ); + + const result = await this.processUrl(url); + this.processedUrls.add(url); + + // Add newly discovered URLs to next level (if not at max depth) + if (this.currentDepth < this.config.maxDepth && result.links) { + for (const link of result.links) { + if (!this.processedUrls.has(link) && !this.urlQueue.has(link)) { + nextLevelUrls.add(link); + } + } + } + + console.log(`[NewCrawler] Processed: ${url} (found ${result.links?.length || 0} links)`); + + } catch (error) { + console.error(`[NewCrawler] Failed to process ${url}:`, error); + this.processedUrls.add(url); // Mark as processed even if failed + } + + // Update progress after each URL + await this.crawlManager.updateCrawlProgress( + this.urlQueue.size + this.processedUrls.size + nextLevelUrls.size, + this.processedUrls.size, + undefined, + "running" + ); + } + + // Clear current queue and add next level URLs + this.urlQueue.clear(); + nextLevelUrls.forEach(url => this.urlQueue.add(url)); + + console.log(`[NewCrawler] Level ${this.currentDepth} complete. Next level has ${this.urlQueue.size} URLs`); + } + + private async processUrl(url: string): Promise<{ links: string[] | null }> { + console.log(`[NewCrawler] Processing URL: ${url}`); + + try { + let html: string; + let title: string = ""; + let description: string = ""; + let links: string[] = []; + + if (this.currentDepth === 0) { + // Use Puppeteer for the root page to handle JavaScript + const browser = await puppeteer.launch({ headless: true }); + const page = await browser.newPage(); + await page.goto(url, { waitUntil: "networkidle2", timeout: 30000 }); + + html = await page.content(); + title = await page.title(); + + const metaDescription = await page + .$eval('meta[name="description"]', (el) => el.getAttribute("content")) + .catch(() => ""); + description = metaDescription || ""; + + links = await page.$$eval("a[href]", (as) => + as.map((a) => (a as HTMLAnchorElement).href) + ); + + await browser.close(); + } else { + // Use fetch for other pages for better performance + const response = await fetch(url, { + redirect: "follow", + headers: { "User-Agent": "CloudLense Crawler" }, + signal: AbortSignal.timeout(10000), // 10 second timeout + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + html = await response.text(); + const dom = new JSDOM(html); + const document = dom.window.document; + + title = document.title; + description = document + .querySelector('meta[name="description"]') + ?.getAttribute("content") || ""; + links = this.extractLinks(document, url); + } + + // Save page to database + await this.savePage(url, title, description, html); + + // Filter and normalize links + const validLinks = this.filterAndNormalizeLinks(links); + + return { links: validLinks }; + + } catch (error) { + console.error(`[NewCrawler] Error processing ${url}:`, error); + throw error; + } + } + + private async savePage(url: string, title: string, description: string, html: string): Promise<void> { + try { + const urlObj = new URL(url); + const contentHash = await this.computeContentHash(html); + + // Check if page already exists + const { data: existingPage } = await getSupabaseAdmin() + .from("pages") + .select("id") + .eq("website_id", this.websiteId) + .eq("url", url) + .single(); + + if (existingPage) { + // Update existing page + await getSupabaseAdmin() + .from("pages") + .update({ + title, + description, + content_hash: contentHash, + last_crawled_at: new Date().toISOString(), + metadata: { + crawl_session_id: this.sessionId, + last_crawl_depth: this.currentDepth, + }, + }) + .eq("id", existingPage.id as string); + } else { + // Insert new page + await getSupabaseAdmin() + .from("pages") + .insert([{ + website_id: this.websiteId, + url, + path: urlObj.pathname, + title, + description, + content_hash: contentHash, + content_type: "text/html", + status_code: 200, + depth: this.currentDepth, + is_active: true, + last_crawled_at: new Date().toISOString(), + metadata: { + crawl_session_id: this.sessionId, + discovery_depth: this.currentDepth, + }, + }]); + } + + } catch (error) { + console.error(`[NewCrawler] Failed to save page ${url}:`, error); + // Don't throw - continue crawling even if save fails + } + } + + private extractLinks(document: Document, baseUrl: string): string[] { + const links = Array.from(document.querySelectorAll("a[href]")) + .map((link) => { + const href = link.getAttribute("href"); + if (!href) return null; + + try { + return new URL(href, baseUrl).href; + } catch { + return null; + } + }) + .filter((url): url is string => url !== null); + + return links; + } + + private filterAndNormalizeLinks(links: string[]): string[] { + const baseUrlObj = new URL(this.baseUrl); + const validLinks: string[] = []; + + for (const link of links) { + try { + const linkObj = new URL(link); + + // Only include links from the same domain + if (linkObj.hostname !== baseUrlObj.hostname) { + continue; + } + + // Remove query parameters and fragments + linkObj.search = ""; + linkObj.hash = ""; + const normalizedUrl = linkObj.href; + + // Apply include/exclude patterns + const path = linkObj.pathname; + const shouldInclude = this.config.includePatterns.some(pattern => + this.matchPattern(path, pattern) + ); + const shouldExclude = this.config.excludePatterns.some(pattern => + this.matchPattern(path, pattern) + ); + + if (shouldInclude && !shouldExclude) { + validLinks.push(normalizedUrl); + } + + } catch (error) { + // Skip invalid URLs + continue; + } + } + + // Remove duplicates and return + return Array.from(new Set(validLinks)); + } + + private matchPattern(path: string, pattern: string): boolean { + const regexPattern = pattern.replace(/\*/g, ".*"); + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(path); + } + + private async computeContentHash(content: string): Promise<string> { + const encoder = new TextEncoder(); + const data = encoder.encode(content); + const hashBuffer = await crypto.subtle.digest("SHA-1", data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); + } + + async cancelCrawl(): Promise<void> { + await this.crawlManager.cancelCrawl(); + } +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/services/notificationService.ts b/website-monitoring-frontend/src/services/notificationService.ts new file mode 100644 index 0000000..cd04788 --- /dev/null +++ b/website-monitoring-frontend/src/services/notificationService.ts @@ -0,0 +1,8 @@ +export class NotificationService { + static async processNotifications() { + const response = await fetch("/api/notifications/process", { + method: "POST", + }); + return response.json(); + } +} diff --git a/website-monitoring-frontend/src/services/scanScheduler.ts b/website-monitoring-frontend/src/services/scanScheduler.ts new file mode 100644 index 0000000..fb938d3 --- /dev/null +++ b/website-monitoring-frontend/src/services/scanScheduler.ts @@ -0,0 +1,353 @@ +import { getSupabaseAdmin } from "@/lib/admin"; +import { lighthouseScanner, ScanConfig } from "./lighthouseScanner"; +import { logError } from "@/utils/errorUtils"; + +export interface ScheduledScan { + id: string; + websiteId: string; + organizationId: string; + frequency: 'hourly' | 'daily' | 'weekly' | 'monthly'; + lastRunAt?: string; + nextRunAt: string; + isActive: boolean; + deviceTypes: ('desktop' | 'mobile')[]; + categories: ('performance' | 'accessibility' | 'seo' | 'best_practices')[]; +} + +export class ScanScheduler { + private supabase = getSupabaseAdmin(); + + /** + * Get all websites that need scheduled scans + */ + async getScheduledScans(): Promise<ScheduledScan[]> { + try { + const { data: websites, error } = await this.supabase + .from('websites') + .select(` + id, + organization_id, + scan_schedule, + is_active, + organizations!inner ( + subscription_tier, + settings + ) + `) + .eq('is_active', true); + + if (error || !websites) { + return []; + } + + const scheduledScans: ScheduledScan[] = []; + + for (const website of websites) { + const scanSchedule = website.scan_schedule || {}; + const subscriptionLimits = this.getSubscriptionLimits((website.organizations as any)?.subscription_tier || 'free'); + + // Check if scheduled scans are enabled for this subscription + if (!subscriptionLimits.scheduledScansEnabled) { + continue; + } + + // Get the last scan for this website + const { data: lastScan } = await this.supabase + .from('scans') + .select('created_at') + .eq('website_id', website.id as string) + .order('created_at', { ascending: false }) + .limit(1) + .single(); + + const lastRunAt = lastScan?.created_at; + const nextRunAt = this.calculateNextRunTime( + (scanSchedule as any).frequency || subscriptionLimits.scanFrequency, + lastRunAt as string | undefined + ); + + // Only include if it's time to run + if (new Date(nextRunAt) <= new Date()) { + scheduledScans.push({ + id: website.id as string, + websiteId: website.id as string, + organizationId: website.organization_id as string, + frequency: (scanSchedule as any).frequency || subscriptionLimits.scanFrequency, + lastRunAt: lastRunAt as string | undefined, + nextRunAt, + isActive: true, + deviceTypes: (scanSchedule as any).device_types || ['desktop'], + categories: (scanSchedule as any).categories || ['performance', 'accessibility', 'seo', 'best_practices'], + }); + } + } + + return scheduledScans; + } catch (error) { + logError('Error getting scheduled scans', error); + return []; + } + } + + /** + * Process all scheduled scans + */ + async processScheduledScans(): Promise<void> { + try { + const scheduledScans = await this.getScheduledScans(); + + for (const scan of scheduledScans) { + await this.processScheduledScan(scan); + } + } catch (error) { + logError('Error processing scheduled scans', error); + } + } + + /** + * Process a single scheduled scan + */ + private async processScheduledScan(scheduledScan: ScheduledScan): Promise<void> { + try { + // Check subscription limits + const { canScan, limits, currentUsage } = await lighthouseScanner.checkSubscriptionLimits( + scheduledScan.organizationId + ); + + if (!canScan) { + logError('Subscription limit exceeded for scheduled scan', null, { + organizationId: scheduledScan.organizationId, + currentUsage, + limits, + }); + return; + } + + // Get the main page for this website + const { data: page, error: pageError } = await this.supabase + .from('pages') + .select('id') + .eq('website_id', scheduledScan.websiteId) + .eq('is_active', true) + .order('created_at', { ascending: false }) + .limit(1) + .single(); + + if (pageError || !page) { + logError('No active page found for scheduled scan', pageError, { + websiteId: scheduledScan.websiteId, + }); + return; + } + + // Perform scans for each device type + for (const deviceType of scheduledScan.deviceTypes) { + const scanConfig: ScanConfig = { + websiteId: scheduledScan.websiteId, + pageId: page.id as string, + deviceType, + categories: scheduledScan.categories, + priority: 'medium', + triggeredBy: 'scheduled', + }; + + await lighthouseScanner.performScan(scanConfig); + } + + // Update the last run time + await this.updateLastRunTime(scheduledScan.websiteId); + } catch (error) { + logError('Error processing scheduled scan', error, { scheduledScan }); + } + } + + /** + * Check for website changes and trigger scans if needed + */ + async processChangeDetection(): Promise<void> { + try { + const { data: websites, error } = await this.supabase + .from('websites') + .select(` + id, + organization_id, + organizations!inner ( + subscription_tier + ) + `) + .eq('is_active', true); + + if (error || !websites) { + return; + } + + for (const website of websites) { + await this.processChangeDetectionForWebsite(website); + } + } catch (error) { + logError('Error processing change detection', error); + } + } + + /** + * Process change detection for a single website + */ + private async processChangeDetectionForWebsite(website: any): Promise<void> { + try { + const subscriptionLimits = this.getSubscriptionLimits(website.organizations.subscription_tier); + + // Check if change detection is enabled for this subscription + if (!subscriptionLimits.changeDetectionEnabled) { + return; + } + + // Check subscription limits + const { canScan } = await lighthouseScanner.checkSubscriptionLimits(website.organization_id); + if (!canScan) { + return; + } + + // Check if there are changes + const hasChanges = await lighthouseScanner.detectChanges(website.id); + if (!hasChanges) { + return; + } + + // Get the main page + const { data: page, error: pageError } = await this.supabase + .from('pages') + .select('id') + .eq('website_id', website.id) + .eq('is_active', true) + .order('created_at', { ascending: false }) + .limit(1) + .single(); + + if (pageError || !page) { + return; + } + + // Trigger a high-priority scan due to changes + const scanConfig: ScanConfig = { + websiteId: website.id, + pageId: page.id as string, + deviceType: 'desktop', // Start with desktop for change detection + categories: ['performance', 'accessibility', 'seo', 'best_practices'], + priority: 'high', + triggeredBy: 'change_detection', + }; + + await lighthouseScanner.performScan(scanConfig); + + // Log the change detection + await this.logChangeDetection(website.id); + } catch (error) { + logError('Error processing change detection for website', error, { websiteId: website.id }); + } + } + + /** + * Calculate the next run time based on frequency + */ + private calculateNextRunTime(frequency: string, lastRunAt?: string): string { + const now = new Date(); + const lastRun = lastRunAt ? new Date(lastRunAt) : new Date(0); + + const nextRun = new Date(lastRun); + + switch (frequency) { + case 'hourly': + nextRun.setHours(nextRun.getHours() + 1); + break; + case 'daily': + nextRun.setDate(nextRun.getDate() + 1); + break; + case 'weekly': + nextRun.setDate(nextRun.getDate() + 7); + break; + case 'monthly': + nextRun.setMonth(nextRun.getMonth() + 1); + break; + default: + nextRun.setDate(nextRun.getDate() + 1); // Default to daily + } + + // If the calculated time is in the past, use now + return nextRun > now ? nextRun.toISOString() : now.toISOString(); + } + + /** + * Update the last run time for a website + */ + private async updateLastRunTime(websiteId: string): Promise<void> { + try { + await this.supabase + .from('websites') + .update({ + last_scan_at: new Date().toISOString(), + }) + .eq('id', websiteId); + } catch (error) { + logError('Error updating last run time', error, { websiteId }); + } + } + + /** + * Log change detection event + */ + private async logChangeDetection(websiteId: string): Promise<void> { + try { + await this.supabase + .from('audit_logs') + .insert({ + website_id: websiteId, + action: 'change_detected', + entity_type: 'website', + entity_id: websiteId, + changes: { change_type: 'content_update' }, + }); + } catch (error) { + logError('Error logging change detection', error, { websiteId }); + } + } + + /** + * Get subscription limits based on tier + */ + private getSubscriptionLimits(tier: string) { + switch (tier) { + case 'free': + return { + scheduledScansEnabled: false, + changeDetectionEnabled: false, + scanFrequency: 'weekly' as const, + }; + case 'starter': + return { + scheduledScansEnabled: true, + changeDetectionEnabled: true, + scanFrequency: 'daily' as const, + }; + case 'professional': + return { + scheduledScansEnabled: true, + changeDetectionEnabled: true, + scanFrequency: 'hourly' as const, + }; + case 'enterprise': + return { + scheduledScansEnabled: true, + changeDetectionEnabled: true, + scanFrequency: 'hourly' as const, + }; + default: + return { + scheduledScansEnabled: false, + changeDetectionEnabled: false, + scanFrequency: 'weekly' as const, + }; + } + } +} + +export const scanScheduler = new ScanScheduler(); \ No newline at end of file diff --git a/website-monitoring-frontend/src/services/scanService.ts b/website-monitoring-frontend/src/services/scanService.ts new file mode 100644 index 0000000..361fd68 --- /dev/null +++ b/website-monitoring-frontend/src/services/scanService.ts @@ -0,0 +1,465 @@ +import { supabase } from "@/lib/supabase"; + +export interface ScanResult { + id: string; + page_id: string; + status: "pending" | "running" | "completed" | "failed"; + performance_score?: number; + seo_score?: number; + accessibility_score?: number; + best_practices_score?: number; + created_at: string; + completed_at?: string; + error_message?: string; +} + +export interface MetricValue { + scan_id: string; + metric_key: string; + value: number; + score?: number; +} + +export interface ScanStats { + totalScans: number; + completedScans: number; + failedScans: number; + averagePerformance: number; + averageSeo: number; + averageAccessibility: number; + averageBestPractices: number; +} + +export class ScanService { + // Start scan for a specific page + async scanPage( + pageId: string, + ): Promise<{ success: boolean; scanId?: string; error?: string }> { + try { + const response = await fetch("/api/analyze", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + pageId, + triggerType: "manual", + }), + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || "Failed to start scan"); + } + + return { + success: true, + scanId: result.scanIds?.[0], + }; + } catch (error) { + console.error("Failed to start page scan:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } + } + + // Start scan for all active pages of a website + async scanWebsite( + websiteId: string, + ): Promise<{ success: boolean; scanIds?: string[]; error?: string }> { + try { + const response = await fetch("/api/analyze", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + websiteId, + triggerType: "manual", + }), + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || "Failed to start website scan"); + } + + return { + success: true, + scanIds: result.scanIds, + }; + } catch (error) { + console.error("Failed to start website scan:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } + } + + // Get scan results for a page + async getPageScans( + pageId: string, + limit: number = 10, + ): Promise<ScanResult[]> { + try { + const { data: scans, error } = await supabase + .from("scans") + .select( + ` + id, + page_id, + status, + created_at, + started_at, + completed_at, + error_message, + scan_results ( + category, + score + ) + `, + ) + .eq("page_id", pageId) + .order("created_at", { ascending: false }) + .limit(limit); + + if (error) throw error; + + return ( + scans?.map((scan) => { + const scores: Record<string, number | undefined> = {}; + (scan.scan_results || []).forEach((result: any) => { + scores[result.category] = result.score; + }); + return { + id: scan.id, + page_id: scan.page_id, + status: scan.status, + performance_score: scores.performance, + seo_score: scores.seo, + accessibility_score: scores.accessibility, + best_practices_score: scores.best_practices, + created_at: scan.created_at, + completed_at: scan.completed_at, + error_message: scan.error_message, + }; + }) || [] + ); + } catch (error) { + console.error("Failed to fetch page scans:", error); + return []; + } + } + + // Get scan results for a website + async getWebsiteScans( + websiteId: string, + limit: number = 50, + ): Promise<ScanResult[]> { + try { + const { data: scans, error } = await supabase + .from("scans") + .select( + ` + id, + page_id, + status, + created_at, + started_at, + completed_at, + error_message, + pages!inner ( + website_id, + url, + title + ), + scan_results ( + category, + score + ) + `, + ) + .eq("pages.website_id", websiteId) + .order("created_at", { ascending: false }) + .limit(limit); + + if (error) throw error; + + return ( + scans?.map((scan) => { + const scores: Record<string, number | undefined> = {}; + (scan.scan_results || []).forEach((result: any) => { + scores[result.category] = result.score; + }); + return { + id: scan.id, + page_id: scan.page_id, + status: scan.status, + performance_score: scores.performance, + seo_score: scores.seo, + accessibility_score: scores.accessibility, + best_practices_score: scores.best_practices, + created_at: scan.created_at, + completed_at: scan.completed_at, + error_message: scan.error_message, + }; + }) || [] + ); + } catch (error) { + console.error("Failed to fetch website scans:", error); + return []; + } + } + + // Get detailed scan result + async getScanDetails(scanId: string) { + try { + const { data: scan, error } = await supabase + .from("scans") + .select( + ` + *, + pages ( + id, + url, + title, + website_id + ), + scan_results (*), + metric_values (*) + `, + ) + .eq("id", scanId) + .single(); + + if (error) throw error; + return scan; + } catch (error) { + console.error("Failed to fetch scan details:", error); + return null; + } + } + + // Get scan statistics for a website + async getWebsiteStats(websiteId: string): Promise<ScanStats> { + try { + const { data: scans, error } = await supabase + .from("scans") + .select( + ` + id, + status, + scan_results ( + category, + score + ) + `, + ) + .eq("website_id", websiteId) + .neq("scan_results", null); + + if (error) throw error; + + const totalScans = scans?.length || 0; + const completedScans = + scans?.filter((s) => s.status === "completed").length || 0; + const failedScans = + scans?.filter((s) => s.status === "failed").length || 0; + + const completedResults = + scans?.filter( + (s) => s.status === "completed" && s.scan_results?.length > 0, + ) || []; + + const getAvg = (cat: string) => { + const vals = completedResults.map((scan) => { + const found = (scan.scan_results || []).find((r: any) => r.category === cat); + return found ? found.score : 0; + }); + return vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0; + }; + + return { + totalScans, + completedScans, + failedScans, + averagePerformance: Math.round(getAvg("performance")), + averageSeo: Math.round(getAvg("seo")), + averageAccessibility: Math.round(getAvg("accessibility")), + averageBestPractices: Math.round(getAvg("best_practices")), + }; + } catch (error) { + console.error("Failed to fetch website stats:", error); + return { + totalScans: 0, + completedScans: 0, + failedScans: 0, + averagePerformance: 0, + averageSeo: 0, + averageAccessibility: 0, + averageBestPractices: 0, + }; + } + } + + // Get recent scans across all websites for dashboard + async getRecentScans(limit: number = 20): Promise<ScanResult[]> { + try { + const { data: scans, error } = await supabase + .from("scans") + .select( + ` + id, + website_id, + page_id, + status, + created_at, + completed_at, + error_message, + pages ( + url, + title, + websites ( + name + ) + ), + scan_results ( + category, + score + ) + `, + ) + .order("created_at", { ascending: false }) + .limit(limit); + + if (error) { + console.error("Error fetching recent scans:", error); + // If scans table doesn't exist, return empty array + if (error.message?.includes("does not exist")) { + return []; + } + throw error; + } + + return ( + scans?.map((scan) => { + const scores: Record<string, number | null> = {}; + (scan.scan_results || []).forEach((result: any) => { + scores[result.category] = result.score; + }); + return { + id: scan.id, + page_id: scan.page_id, + status: scan.status, + performance_score: scores.performance ?? undefined, + seo_score: scores.seo ?? undefined, + accessibility_score: scores.accessibility ?? undefined, + best_practices_score: scores.best_practices ?? undefined, + created_at: scan.created_at, + completed_at: scan.completed_at, + error_message: scan.error_message, + }; + }) || [] + ); + } catch (error) { + console.error("Failed to fetch recent scans:", error); + return []; + } + } + + // Monitor scan progress + async monitorScanProgress( + scanIds: string[], + ): Promise<{ [scanId: string]: string }> { + try { + const { data: scans, error } = await supabase + .from("scans") + .select("id, status") + .in("id", scanIds); + + if (error) throw error; + + const statusMap: { [scanId: string]: string } = {}; + scans?.forEach((scan) => { + statusMap[scan.id] = scan.status; + }); + + return statusMap; + } catch (error) { + console.error("Failed to monitor scan progress:", error); + return {}; + } + } + + // Subscribe to scan updates + subscribeToScanUpdates(callback: (payload: any) => void, websiteId?: string) { + let subscription; + + if (websiteId) { + // Subscribe to scans for a specific website + subscription = supabase + .channel("website-scans") + .on( + "postgres_changes", + { + event: "*", + schema: "public", + table: "scans", + filter: `pages.website_id=eq.${websiteId}`, + }, + callback, + ) + .subscribe(); + } else { + // Subscribe to all scan updates + subscription = supabase + .channel("all-scans") + .on( + "postgres_changes", + { + event: "*", + schema: "public", + table: "scans", + }, + callback, + ) + .subscribe(); + } + + return subscription; + } + + // Utility function to get score color + getScoreColor(score: number): string { + if (score >= 90) return "text-green-600"; + if (score >= 70) return "text-yellow-600"; + return "text-red-600"; + } + + // Utility function to get score badge color + getScoreBadgeColor(score: number): string { + if (score >= 90) return "bg-green-100 text-green-800"; + if (score >= 70) return "bg-yellow-100 text-yellow-800"; + return "bg-red-100 text-red-800"; + } + + // Format scan duration + formatScanDuration(startTime: string, endTime?: string): string { + const start = new Date(startTime); + const end = endTime ? new Date(endTime) : new Date(); + const diffMs = end.getTime() - start.getTime(); + const diffSeconds = Math.round(diffMs / 1000); + + if (diffSeconds < 60) return `${diffSeconds}s`; + if (diffSeconds < 3600) return `${Math.round(diffSeconds / 60)}m`; + return `${Math.round(diffSeconds / 3600)}h`; + } +} + +// Export singleton instance +export const scanService = new ScanService(); diff --git a/website-monitoring-frontend/src/services/serverCrawlManager.ts b/website-monitoring-frontend/src/services/serverCrawlManager.ts new file mode 100644 index 0000000..2af329a --- /dev/null +++ b/website-monitoring-frontend/src/services/serverCrawlManager.ts @@ -0,0 +1,173 @@ +import { getSupabaseAdmin } from "@/lib/admin"; + +interface CrawlStatus { + id: string; + website_id: string; + status: "pending" | "running" | "completed" | "failed" | "cancelled"; + total_urls: number; + processed_urls: number; + progress_percentage: number; + current_url?: string; + started_at: string; + completed_at?: string; + error_message?: string; +} + +interface CrawlProgress { + totalUrls: number; + processedUrls: number; + progressPercentage: number; + currentUrl?: string; + status: "pending" | "running" | "completed" | "failed" | "cancelled"; +} + +// Server-side crawl manager (uses admin functions) +export class ServerCrawlManager { + private sessionId: string; + private websiteId: string; + private progressCallback?: (progress: CrawlProgress) => void; + + constructor(websiteId: string, sessionId: string) { + this.websiteId = websiteId; + this.sessionId = sessionId; + } + + // Set a callback function to receive real-time progress updates + setProgressCallback(callback: (progress: CrawlProgress) => void) { + this.progressCallback = callback; + } + + // Initialize a new crawl session + async initializeCrawl(): Promise<void> { + await getSupabaseAdmin() + .from("crawl_sessions") + .update({ + status: "pending", + total_urls: 0, + processed_urls: 0, + progress_percentage: 0, + started_at: new Date().toISOString(), + }) + .eq("id", this.sessionId); + + this.updateProgress({ + totalUrls: 0, + processedUrls: 0, + progressPercentage: 0, + status: "pending", + }); + } + + // Update crawl progress and notify listeners + async updateCrawlProgress( + totalUrls: number, + processedUrls: number, + currentUrl?: string, + status?: "pending" | "running" | "completed" | "failed" | "cancelled" + ): Promise<void> { + const progressPercentage = totalUrls > 0 ? Math.round((processedUrls / totalUrls) * 100) : 0; + + // Update database + const updateData: any = { + total_urls: totalUrls, + processed_urls: processedUrls, + progress_percentage: progressPercentage, + }; + + if (currentUrl) { + updateData.current_url = currentUrl; + } + + if (status) { + updateData.status = status; + if (status === "completed" || status === "failed" || status === "cancelled") { + updateData.completed_at = new Date().toISOString(); + } + } + + await getSupabaseAdmin() + .from("crawl_sessions") + .update(updateData) + .eq("id", this.sessionId); + + // Notify listeners + this.updateProgress({ + totalUrls, + processedUrls, + progressPercentage, + currentUrl, + status: status || "running", + }); + } + + // Get current crawl status + async getCrawlStatus(): Promise<CrawlStatus | null> { + try { + const { data, error } = await getSupabaseAdmin() + .from("crawl_sessions") + .select("*") + .eq("id", this.sessionId) + .single(); + + if (error || !data) { + return null; + } + + return { + id: data.id, + website_id: data.website_id, + status: data.status, + total_urls: data.total_urls || 0, + processed_urls: data.processed_urls || 0, + progress_percentage: data.progress_percentage || 0, + current_url: data.current_url, + started_at: data.started_at, + completed_at: data.completed_at, + error_message: data.error_message, + } as CrawlStatus; + } catch (error) { + console.error("Failed to get crawl status:", error); + return null; + } + } + + // Complete the crawl session + async completeCrawl(success: boolean = true, errorMessage?: string): Promise<void> { + const status = success ? "completed" : "failed"; + const updateData: any = { + status, + completed_at: new Date().toISOString(), + }; + + if (errorMessage) { + updateData.error_message = errorMessage; + } + + await getSupabaseAdmin() + .from("crawl_sessions") + .update(updateData) + .eq("id", this.sessionId); + + this.updateProgress({ + totalUrls: 0, + processedUrls: 0, + progressPercentage: 0, + status, + }); + } + + async cancelCrawl(): Promise<void> { + await this.completeCrawl(false, "Cancelled by user"); + } + + private updateProgress(progress: CrawlProgress) { + if (this.progressCallback) { + this.progressCallback(progress); + } + } +} + +// Factory function to create server crawl manager instances +export function createServerCrawlManager(websiteId: string, sessionId: string): ServerCrawlManager { + return new ServerCrawlManager(websiteId, sessionId); +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/services/websiteService.ts b/website-monitoring-frontend/src/services/websiteService.ts new file mode 100644 index 0000000..2f6e8cd --- /dev/null +++ b/website-monitoring-frontend/src/services/websiteService.ts @@ -0,0 +1,256 @@ +import { supabase } from "@/lib/supabase"; + +interface Website { + id: string; + name: string; + base_url: string; + is_active: boolean; + created_at: string; + organization_id: string; + settings?: Record<string, unknown>; +} + +export const websiteService = { + async addWebsite(website: Omit<Website, "id" | "created_at">) { + const { data, error } = await supabase + .from("websites") + .insert([ + { + base_url: website.base_url, + name: website.name, + organization_id: website.organization_id, + is_active: true, + settings: website.settings || {}, + }, + ]) + .select() + .single(); + + if (error) throw error; + return data; + }, + + async getWebsites(organizationId: string, userId: string) { + console.log("🔍 getWebsites called with:", { organizationId, userId }); + + if (!organizationId || !userId) { + throw new Error("Organization ID and User ID are required"); + } + + try { + // Simple approach: just get the websites for the organization + console.log("📊 Fetching websites for organization:", organizationId); + + const { data: websites, error: websitesError } = await supabase + .from("websites") + .select("*") + .eq("organization_id", organizationId) + .order("created_at", { ascending: false }); + + if (websitesError) { + console.error("❌ Websites query error:", websitesError); + throw new Error(`Database error: ${websitesError.message}`); + } + + console.log("✅ Websites fetched successfully:", websites?.length || 0); + + // Return websites with basic stats structure + const websitesWithStats = (websites || []).map(website => ({ + ...website, + stats: { + pagesCount: 0, // Will be populated when pages table is available + latestScan: null, // Will be populated when scans table is available + } + })); + + return websitesWithStats; + } catch (error) { + console.error("❌ Error in getWebsites:", error); + + // Better error handling + if (error instanceof Error) { + throw error; + } else { + throw new Error(`Unexpected error: ${JSON.stringify(error)}`); + } + } + }, + + async getWebsite(websiteId: string) { + const { data: website, error } = await supabase + .from("websites") + .select("*") + .eq("id", websiteId) + .single(); + + if (error) throw error; + + // Get additional statistics + const [pagesResult, scansResult, latestScanResult] = await Promise.all([ + supabase + .from("pages") + .select("id", { count: "exact", head: true }) + .eq("website_id", websiteId), + supabase + .from("scans") + .select("id", { count: "exact", head: true }) + .eq("website_id", websiteId), + supabase + .from("scans") + .select("*") + .eq("website_id", websiteId) + .order("created_at", { ascending: false }) + .limit(1), + ]); + + return { + ...website, + stats: { + pagesCount: pagesResult.count || 0, + scansCount: scansResult.count || 0, + latestScan: latestScanResult.data?.[0] || null, + }, + }; + }, + + async getWebsiteSettings(websiteId: string, userId: string) { + const response = await fetch(`/api/websites/${websiteId}/settings?userId=${userId}`); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to fetch website settings"); + } + + const data = await response.json(); + return data.settings; + }, + + async updateScanSettings( + websiteId: string, + settings: Record<string, unknown>, + userId: string, + ) { + const response = await fetch(`/api/websites/${websiteId}/settings`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + userId, + settingsType: "scan", + settings, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to update scan settings"); + } + + return response.json(); + }, + + async updateAlertSettings( + websiteId: string, + settings: Record<string, unknown>, + userId: string, + ) { + const response = await fetch(`/api/websites/${websiteId}/settings`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + userId, + settingsType: "alerts", + settings, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to update alert settings"); + } + + return response.json(); + }, + + async updateWebsite( + websiteId: string, + updates: { name?: string; base_url?: string; is_active?: boolean; crawl_settings?: any }, + userId: string, + ) { + const response = await fetch("/api/websites", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: websiteId, + userId, + ...updates, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to update website"); + } + + const data = await response.json(); + return data.website; + }, + + async deleteWebsite(websiteId: string, userId: string) { + const response = await fetch(`/api/websites?id=${websiteId}&userId=${userId}`, { + method: "DELETE", + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to delete website"); + } + + return response.json(); + }, + + async setupMonitoring(websiteId: string, config: Record<string, unknown>) { + // Create scan configuration + const { error: scanError } = await supabase + .from("scan_configurations") + .insert([ + { + website_id: websiteId, + category: "performance", + interval_minutes: 60, // Default hourly + is_active: true, + priority: 1, + settings: { + lighthouse: true, + uptime: true, + ...config, + }, + }, + ]); + + if (scanError) throw scanError; + + // Set up initial alert configurations + const { error: alertError } = await supabase + .from("alert_configurations") + .insert([ + { + website_id: websiteId, + metric: "performance", + threshold: 90, + comparison: "less_than", + notification_channels: ["email"], + is_active: true, + }, + { + website_id: websiteId, + metric: "uptime", + threshold: 99.9, + comparison: "less_than", + notification_channels: ["email"], + is_active: true, + }, + ]); + + if (alertError) throw alertError; + }, +}; diff --git a/website-monitoring-frontend/src/types/auth.ts b/website-monitoring-frontend/src/types/auth.ts new file mode 100644 index 0000000..2ca5734 --- /dev/null +++ b/website-monitoring-frontend/src/types/auth.ts @@ -0,0 +1,32 @@ +export interface UserMetadata { + name: string; + organization_id: string; + role: "owner" | "admin" | "editor" | "viewer"; +} + +export interface Organization { + id: string; + name: string; + subscription_tier: string; + subscription_status: string; + settings: { + alert_email_digest: string; + default_scan_depth: number; + retention_days: number; + enable_competitor_analysis: boolean; + }; +} + +export interface UserProfile { + id: string; + email: string; + name: string; + organization_id: string; + role: string; + is_active: boolean; + settings: { + email_notifications: boolean; + notification_frequency: string; + dashboard_layout: string; + }; +} diff --git a/website-monitoring-frontend/src/types/crawler.ts b/website-monitoring-frontend/src/types/crawler.ts new file mode 100644 index 0000000..c775345 --- /dev/null +++ b/website-monitoring-frontend/src/types/crawler.ts @@ -0,0 +1,39 @@ +export interface CrawlQueueItem { + id: string; + website_id: string; + url: string; + discovery_depth: number; + parent_url?: string; + status: string; + priority: number; + attempts?: number; + created_at?: string; + updated_at?: string; +} + +export interface PageData { + id: string; + website_id: string; + url: string; + title?: string; + status?: string; + last_scanned_at?: string; + scan_count?: number; + metadata?: any; + created_at?: string; + updated_at?: string; +} + +export interface WebsiteData { + id: string; + organization_id: string; + name: string; + base_url: string; + crawl_settings?: any; + alert_settings?: any; + last_crawled_at?: string; + crawl_session_id?: string; + metadata?: any; + created_at?: string; + updated_at?: string; +} diff --git a/website-monitoring-frontend/src/types/database.ts b/website-monitoring-frontend/src/types/database.ts new file mode 100644 index 0000000..2af422e --- /dev/null +++ b/website-monitoring-frontend/src/types/database.ts @@ -0,0 +1,75 @@ +export interface Scan { + id: string; + website_id: string; + page_id?: string; + status: 'pending' | 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'; + scan_type: string; + trigger_type: string; + priority: number; + categories: string[]; + device_type: string; + user_agent?: string; + lighthouse_version?: string; + chrome_version?: string; + environment?: string; + started_at?: string; + completed_at?: string; + duration_ms?: number; + error_message?: string; + retry_count: number; + metadata?: Record<string, any>; + created_at: string; + updated_at: string; + performance_score?: number; + seo_score?: number; + accessibility_score?: number; + best_practices_score?: number; +} + +export interface Page { + id: string; + website_id: string; + url: string; + title?: string; + status: 'pending' | 'scanned' | 'failed'; + last_scanned_at?: string; + created_at: string; + updated_at: string; +} + +export interface Website { + id: string; + name: string; + base_url: string; + is_active: boolean; + organization_id: string; + created_at: string; + updated_at: string; + settings?: Record<string, any>; +} + +export interface User { + id: string; + email: string; + name: string; + organization_id: string; + role: string; + is_active: boolean; + created_at: string; + updated_at: string; +} + +export interface Organization { + id: string; + name: string; + subscription_tier: string; + subscription_status: string; + settings: { + alert_email_digest: string; + default_scan_depth: number; + retention_days: number; + enable_competitor_analysis: boolean; + }; + created_at: string; + updated_at: string; +} diff --git a/website-monitoring-frontend/src/types/jsdom.d.ts b/website-monitoring-frontend/src/types/jsdom.d.ts new file mode 100644 index 0000000..4edfe41 --- /dev/null +++ b/website-monitoring-frontend/src/types/jsdom.d.ts @@ -0,0 +1 @@ +declare module 'jsdom'; diff --git a/website-monitoring-frontend/src/types/lighthouse.ts b/website-monitoring-frontend/src/types/lighthouse.ts new file mode 100644 index 0000000..235fd15 --- /dev/null +++ b/website-monitoring-frontend/src/types/lighthouse.ts @@ -0,0 +1,58 @@ +export interface LighthouseMetrics { + // Core Metrics + performance: number; + accessibility: number; + seo: number; + bestPractices: number; + + // Performance Metrics + firstContentfulPaint: number; + largestContentfulPaint: number; + totalBlockingTime: number; + cumulativeLayoutShift: number; + speedIndex: number; + interactive: number; + + // Network & Resources + totalByteWeight: number; + resourceSummary: { + total: number; + script: number; + image: number; + stylesheet: number; + font: number; + other: number; + document: number; + media: number; + }; + + // Server + serverResponseTime: number; + networkRtt: number; + networkServerLatency: number; + + // Other + mainThreadTasks: Array<{ + duration: number; + startTime: number; + }>; + diagnostics: { + numRequests: number; + numScripts: number; + numStylesheets: number; + numFonts: number; + totalTaskTime: number; + }; + + // Additional Metrics + usesHttps: boolean; + redirectsHttp: boolean; + viewportMetaTag: boolean; + errorsInConsole: boolean; + usesRelPreconnect: boolean; + usesPassiveEventListeners: boolean; + metaDescription: boolean; + httpStatusCode: number; + isCrawlable: boolean; + robotsTxt: boolean; +} diff --git a/website-monitoring-frontend/src/types/lighthouseDescriptions.ts b/website-monitoring-frontend/src/types/lighthouseDescriptions.ts new file mode 100644 index 0000000..2630ad9 --- /dev/null +++ b/website-monitoring-frontend/src/types/lighthouseDescriptions.ts @@ -0,0 +1,55 @@ +export const lighthouseDescriptions = { + // Core Metrics + performance: "Overall performance score of the website, indicating how well the page performs on various metrics.", + accessibility: "Overall accessibility score of the website, indicating how accessible the page is to users with disabilities.", + seo: "Overall SEO score of the website, indicating how well the page is optimized for search engines.", + bestPractices: "Overall best practices score of the website, indicating how well the page follows web development best practices.", + + // Performance Metrics + firstContentfulPaint: "Time when the first text or image is painted, indicating how quickly the page starts to load.", + largestContentfulPaint: "Time when the largest text or image is painted, indicating how quickly the main content of the page loads.", + totalBlockingTime: "Sum of all time periods between FCP and Time to Interactive when task length exceeded 50ms, indicating how much time the page is blocked from responding to user input.", + cumulativeLayoutShift: "Measures the movement of visible elements within the viewport, indicating how stable the page layout is.", + speedIndex: "How quickly the contents of a page are visibly populated, indicating the overall loading speed of the page.", + interactive: "Time when the page becomes fully interactive, indicating how quickly the page is ready for user interaction.", + + // Network & Resources + totalByteWeight: "Total size of all resources loaded by the page, indicating the overall weight of the page.", + resourceSummary: { + total: "Total number of resources loaded by the page.", + script: "Number of script resources loaded by the page.", + image: "Number of image resources loaded by the page.", + stylesheet: "Number of stylesheet resources loaded by the page.", + font: "Number of font resources loaded by the page.", + other: "Number of other resources loaded by the page.", + document: "Number of document resources loaded by the page.", + media: "Number of media resources loaded by the page." + }, + + // Server + serverResponseTime: "Time taken for the server to respond to the initial request, indicating the server's responsiveness.", + networkRtt: "Round-trip time for network requests, indicating the latency of the network.", + networkServerLatency: "Time taken for the server to respond to network requests, indicating the server's latency.", + + // Other + mainThreadTasks: "Array of tasks executed on the main thread, including their duration and start time.", + diagnostics: { + numRequests: "Number of network requests made by the page.", + numScripts: "Number of script resources loaded by the page.", + numStylesheets: "Number of stylesheet resources loaded by the page.", + numFonts: "Number of font resources loaded by the page.", + totalTaskTime: "Total time spent executing tasks on the main thread." + }, + + // Additional Metrics + usesHttps: "Indicates whether the page uses HTTPS for secure communication.", + redirectsHttp: "Indicates whether the page redirects HTTP requests to HTTPS.", + viewportMetaTag: "Indicates whether the page has a viewport meta tag for responsive design.", + errorsInConsole: "Indicates whether there are any errors logged in the browser console.", + usesRelPreconnect: "Indicates whether the page uses rel=preconnect to establish early connections to required origins.", + usesPassiveEventListeners: "Indicates whether the page uses passive event listeners to improve scrolling performance.", + metaDescription: "Indicates whether the page has a meta description for better SEO.", + httpStatusCode: "HTTP status code of the page, indicating the success or failure of the request.", + isCrawlable: "Indicates whether the page is crawlable by search engines.", + robotsTxt: "Indicates whether the page has a robots.txt file for controlling search engine crawling." +}; \ No newline at end of file diff --git a/website-monitoring-frontend/src/types/metrics.ts b/website-monitoring-frontend/src/types/metrics.ts new file mode 100644 index 0000000..1a8ffe2 --- /dev/null +++ b/website-monitoring-frontend/src/types/metrics.ts @@ -0,0 +1,172 @@ +// Base Metrics Interface +export interface BaseMetrics { + performance: number; + seo: number; + accessibility: number; + bestPractices: number; +} + +// CoreWebVitals interface +export interface CoreWebVitals { + LCP: string; + FID: string; + CLS: string; +} + +// Device Related Metrics +export interface DeviceMetrics { + Performance: number; + LCP: number; + TBT: number; + CLS: number; +} + +// Resource Related Metrics +export interface ResourceMetrics { + total: number; + script: number; + image: number; + stylesheet: number; + font: number; + other: number; + document: number; + media: number; + [key: string]: number; +} + +// SEO Related Metrics +export interface SeoMetrics { + metaTitle: boolean; + metaDescription: boolean; + headings: number; + internalLinks: number; + externalLinks: number; + imageAlt: number; + canonical: boolean; + robotsTxt: boolean; +} + +// Accessibility Related Metrics +export interface AccessibilityMetrics { + ariaLabels: number; + contrastRatio: string; + keyboardNav: boolean; + altTexts: number; + headingStructure: boolean; +} + +// Performance Related Metrics +export interface PerformanceMetrics { + firstContentfulPaint: string; + speedIndex: string; + timeToInteractive: string; + totalBlockingTime: string; + largestContentfulPaint: string; + cumulativeLayoutShift: string; +} + +export interface BestPracticesMetrics { + bestPractices: number; +} + +// Critical Issues +export interface CriticalIssue { + issue: string; + metric: string; + severity: "high" | "medium" | "low"; + description?: string; + recommendation?: string; + impact?: number; + type?: string; +} + +// Timeline Events +export interface TimelineEvent { + name: string; + time: number; + color: string; +} + +// Recommendation Interface +export interface Recommendation { + title: string; + description: string; + impact: "high" | "medium" | "low"; + category: "performance" | "seo" | "accessibility" | "best-practices"; +} + +// Dashboard Specific Types +export interface DashboardMetrics extends BaseMetrics { + history: { + performance: number[]; + uptime: number[]; + dates: string[]; + }; + resources: ResourceMetrics; + devices: { + desktop: DeviceMetrics; + mobile: DeviceMetrics; + }; + timeline: TimelineEvent[]; + criticalIssues: CriticalIssue[]; +} + +// Competitor Specific Types +export interface CompetitorMetrics extends BaseMetrics { + name: string; + history: { + performance: number[]; + seo: number[]; + accessibility: number[]; + bestPractices: number[]; + }; + metrics: { + seo: SeoMetrics; + accessibility: AccessibilityMetrics; + performance: PerformanceMetrics; + bestPractices: BestPracticesMetrics; + }; + coreWebVitals: CoreWebVitals; + recommendations: Recommendation[]; +} + +export interface CompetitorData { + yourSite: CompetitorMetrics; + competitors: CompetitorMetrics[]; +} + +// Chart Related Types +export interface ChartDataset { + label: string; + data: number[]; + color: string; + backgroundColor?: string; + borderWidth?: number; + fill?: boolean; + order?: number; +} + +export interface ChartOptions { + responsive?: boolean; + maintainAspectRatio?: boolean; + plugins?: { + legend?: { + position?: "top" | "bottom" | "left" | "right"; + align?: "start" | "center" | "end"; + }; + tooltip?: { + enabled?: boolean; + }; + }; + scales?: { + x?: { + grid?: { + display?: boolean; + }; + }; + y?: { + beginAtZero?: boolean; + max?: number; + }; + }; +} diff --git a/website-monitoring-frontend/src/types/monitoring.ts b/website-monitoring-frontend/src/types/monitoring.ts new file mode 100644 index 0000000..5c1c629 --- /dev/null +++ b/website-monitoring-frontend/src/types/monitoring.ts @@ -0,0 +1,35 @@ +export interface Scan { + id: string; + website_id: string; + status: "pending" | "running" | "completed" | "failed"; + created_at: string; + updated_at: string; + error_message?: string; +} + +export interface Alert { + id: string; + website_id: string; + title: string; + message: string; + severity: "high" | "medium" | "low"; + status: "open" | "acknowledged" | "resolved"; + created_at: string; +} + +export interface MetricValue { + id: string; + scan_id: string; + metric_key: string; + value: number; + timestamp: string; +} + +export interface ResourceUsageData { + id: string; + scan_id: string; + resource_type: string; + size_bytes: number; + transfer_size_bytes: number; + duration_ms: number; +} diff --git a/website-monitoring-frontend/src/utils/authHelpers.ts b/website-monitoring-frontend/src/utils/authHelpers.ts new file mode 100644 index 0000000..a3bbcd8 --- /dev/null +++ b/website-monitoring-frontend/src/utils/authHelpers.ts @@ -0,0 +1,30 @@ +import { supabase } from "@/lib/supabase"; + +export async function syncOrganizationId() { + try { + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) return; + + // Get the organization ID from the users table (the correct one) + const { data: userData } = await supabase + .from("users") + .select("organization_id") + .eq("id", user.id) + .single(); + + if (userData?.organization_id) { + // Update the user metadata with the correct organization_id + await supabase.auth.updateUser({ + data: { + organization_id: userData.organization_id, + }, + }); + + console.log("Organization ID synced successfully"); + } + } catch (error) { + console.error("Failed to sync organization ID:", error); + } +} diff --git a/website-monitoring-frontend/src/utils/dataGenerators.ts b/website-monitoring-frontend/src/utils/dataGenerators.ts new file mode 100644 index 0000000..ee1a0eb --- /dev/null +++ b/website-monitoring-frontend/src/utils/dataGenerators.ts @@ -0,0 +1,515 @@ +import type { DashboardMetrics } from "@/types/metrics"; +import type { CompetitorData } from "@/types/metrics"; +import { COLORS } from "@/constants/colors"; + +export const calculateOverallScore = (metrics: { + performance: number; + seo: number; + accessibility: number; + bestPractices: number; +}): number => { + return ( + Math.round( + (metrics.performance * 0.4 + + metrics.seo * 0.2 + + metrics.accessibility * 0.2 + + metrics.bestPractices * 0.2) * + 10, + ) / 10 + ); +}; + +export const dataGenerators = { + dashboard: { + generate: (): DashboardMetrics => ({ + performance: 75, + seo: 0.92, + accessibility: 0.88, + bestPractices: 0.95, + history: { + performance: Array(24) + .fill(0) + .map(() => Math.round(70 + Math.random() * 20)), + uptime: Array(24) + .fill(0) + .map(() => 98 + Math.random() * 2), + dates: Array.from({ length: 24 }, (_, i) => `${i}:00`), + }, + resources: { + total: 1036, + script: 412, + image: 114, + stylesheet: 82, + font: 48, + other: 46, + document: 102, + media: 232, + }, + devices: { + desktop: { + Performance: 85, + LCP: 82, + TBT: 88, + CLS: 90, + }, + mobile: { + Performance: 75, + LCP: 70, + TBT: 78, + CLS: 80, + }, + }, + timeline: [ + { name: "FCP", time: 0.9, color: COLORS.status.success }, + { name: "LCP", time: 2.3, color: COLORS.status.info }, + { name: "TTI", time: 3.8, color: COLORS.status.warning }, + { name: "Complete", time: 4.7, color: COLORS.status.info }, + ], + criticalIssues: [ + { + issue: "Largest Contentful Paint too slow", + metric: "3.2s", + severity: "high", + }, + { + issue: "Render-blocking resources", + metric: "4 resources", + severity: "medium", + }, + { + issue: "Unused JavaScript", + metric: "240KB", + severity: "medium", + }, + ], + }), + + update: (prevMetrics: DashboardMetrics): DashboardMetrics => { + const MAX_HISTORY = 24; // Define maximum number of data points to keep + + // Update performance history + const newPerformance = [...prevMetrics.history.performance.slice(1)]; + newPerformance.push(Math.round(70 + Math.random() * 20)); + + // Update uptime history + const newUptime = [...prevMetrics.history.uptime.slice(1)]; + newUptime.push(98 + Math.random() * 2); + + // Update dates + const newDates = [...prevMetrics.history.dates.slice(1)]; + const lastDate = + prevMetrics.history.dates[prevMetrics.history.dates.length - 1]; + const hour = parseInt(lastDate.split(":")[0]); + const nextHour = (hour + 1) % 24; + newDates.push(`${String(nextHour).padStart(2, "0")}:00`); + + // Update timeline events with slightly adjusted times + const newTimeline = prevMetrics.timeline + .map((event) => ({ + ...event, + time: Math.max( + 0.1, + Math.min(10, event.time + (Math.random() * 0.4 - 0.2)), + ), + })) + .sort((a, b) => a.time - b.time); + + return { + ...prevMetrics, + performance: Math.max( + 65, + Math.min(95, prevMetrics.performance + (Math.random() * 3 - 1.5)), + ), + seo: Math.max( + 0.7, + Math.min(0.99, prevMetrics.seo + (Math.random() * 0.02 - 0.01)), + ), + accessibility: Math.max( + 0.7, + Math.min( + 0.99, + prevMetrics.accessibility + (Math.random() * 0.02 - 0.01), + ), + ), + bestPractices: Math.max( + 0.7, + Math.min( + 0.99, + prevMetrics.bestPractices + (Math.random() * 0.02 - 0.01), + ), + ), + history: { + performance: newPerformance.slice(-MAX_HISTORY), + uptime: newUptime.slice(-MAX_HISTORY), + dates: newDates.slice(-MAX_HISTORY), + }, + timeline: newTimeline, + devices: { + desktop: { + Performance: Math.round(80 + Math.random() * 15), + LCP: Math.round(80 + Math.random() * 15), + TBT: Math.round(80 + Math.random() * 15), + CLS: Math.round(80 + Math.random() * 15), + }, + mobile: { + Performance: Math.round(65 + Math.random() * 15), + LCP: Math.round(65 + Math.random() * 15), + TBT: Math.round(65 + Math.random() * 15), + CLS: Math.round(65 + Math.random() * 15), + }, + }, + }; + }, + }, + + competitor: { + generate: (): CompetitorData => ({ + yourSite: { + name: "Your Website", + performance: 87, + seo: 92, + accessibility: 89, + bestPractices: 95, + history: { + performance: [82, 84, 85, 83, 87, 86, 87], + seo: [88, 89, 90, 91, 92, 92, 92], + accessibility: [85, 86, 87, 88, 89, 89, 89], + bestPractices: [90, 91, 92, 93, 94, 95, 95], + }, + metrics: { + seo: { + metaTitle: true, + metaDescription: true, + headings: 12, + internalLinks: 24, + externalLinks: 8, + imageAlt: 15, + canonical: true, + robotsTxt: true, + }, + accessibility: { + ariaLabels: 28, + contrastRatio: "4.5:1", + keyboardNav: true, + altTexts: 15, + headingStructure: true, + }, + performance: { + firstContentfulPaint: "1.2s", + speedIndex: "2.8s", + timeToInteractive: "3.2s", + totalBlockingTime: "200ms", + largestContentfulPaint: "2.4s", + cumulativeLayoutShift: "0.1", + }, + bestPractices: { + bestPractices: 95, + }, + }, + coreWebVitals: { + LCP: "2.4s", + FID: "100ms", + CLS: "0.1", + }, + recommendations: [ + { + title: "Optimize image loading", + description: "Implement lazy loading for images below the fold", + impact: "high", + category: "performance", + }, + { + title: "Improve meta descriptions", + description: "Add unique meta descriptions to all important pages", + impact: "medium", + category: "seo", + }, + ], + }, + competitors: [ + { + name: "TechGiant Corp", + performance: 92, + seo: 94, + accessibility: 88, + bestPractices: 96, + history: { + performance: [88, 89, 90, 91, 92, 92, 92], + seo: [90, 91, 92, 93, 94, 94, 94], + accessibility: [85, 85, 86, 87, 88, 88, 88], + bestPractices: [93, 94, 94, 95, 95, 96, 96], + }, + metrics: { + seo: { + metaTitle: true, + metaDescription: true, + headings: 15, + internalLinks: 32, + externalLinks: 12, + imageAlt: 20, + canonical: true, + robotsTxt: true, + }, + accessibility: { + ariaLabels: 25, + contrastRatio: "5:1", + keyboardNav: true, + altTexts: 20, + headingStructure: true, + }, + performance: { + firstContentfulPaint: "0.9s", + speedIndex: "2.2s", + timeToInteractive: "2.8s", + totalBlockingTime: "150ms", + largestContentfulPaint: "2.1s", + cumulativeLayoutShift: "0.08", + }, + bestPractices: { + bestPractices: 96, + }, + }, + coreWebVitals: { + LCP: "2.1s", + FID: "80ms", + CLS: "0.08", + }, + recommendations: [ + { + title: "Improve accessibility scores", + description: + "Enhance ARIA labels and keyboard navigation for better accessibility compliance", + impact: "medium", + category: "accessibility", + }, + { + title: "Optimize third-party scripts", + description: + "Reduce the impact of third-party scripts on page load performance", + impact: "high", + category: "performance", + }, + { + title: "Implement structured data", + description: + "Add more structured data markup for enhanced search results", + impact: "medium", + category: "seo", + }, + ], + }, + { + name: "StartupFlow", + performance: 78, + seo: 88, + accessibility: 92, + bestPractices: 90, + history: { + performance: [75, 76, 77, 77, 78, 78, 78], + seo: [84, 85, 86, 87, 88, 88, 88], + accessibility: [88, 89, 90, 91, 92, 92, 92], + bestPractices: [87, 88, 88, 89, 90, 90, 90], + }, + metrics: { + seo: { + metaTitle: true, + metaDescription: true, + headings: 10, + internalLinks: 18, + externalLinks: 6, + imageAlt: 12, + canonical: true, + robotsTxt: true, + }, + accessibility: { + ariaLabels: 35, + contrastRatio: "7:1", + keyboardNav: true, + altTexts: 12, + headingStructure: true, + }, + performance: { + firstContentfulPaint: "1.8s", + speedIndex: "3.5s", + timeToInteractive: "4.0s", + totalBlockingTime: "300ms", + largestContentfulPaint: "3.2s", + cumulativeLayoutShift: "0.15", + }, + bestPractices: { + bestPractices: 90, + }, + }, + coreWebVitals: { + LCP: "3.2s", + FID: "150ms", + CLS: "0.15", + }, + recommendations: [ + { + title: "Critical rendering path optimization", + description: + "Optimize CSS and JavaScript delivery to improve First Contentful Paint", + impact: "high", + category: "performance", + }, + { + title: "Reduce server response time", + description: + "Optimize backend processing and implement caching strategies", + impact: "high", + category: "performance", + }, + { + title: "Optimize image delivery", + description: "Implement WebP format and proper image sizing", + impact: "medium", + category: "performance", + }, + { + title: "Internal linking structure", + description: + "Improve internal linking strategy for better SEO performance", + impact: "medium", + category: "seo", + }, + ], + }, + { + name: "WebMaster Pro", + performance: 85, + seo: 96, + accessibility: 85, + bestPractices: 92, + history: { + performance: [80, 81, 82, 83, 84, 85, 85], + seo: [92, 93, 94, 95, 95, 96, 96], + accessibility: [82, 83, 83, 84, 85, 85, 85], + bestPractices: [88, 89, 90, 91, 92, 92, 92], + }, + metrics: { + seo: { + metaTitle: true, + metaDescription: true, + headings: 18, + internalLinks: 40, + externalLinks: 15, + imageAlt: 25, + canonical: true, + robotsTxt: true, + }, + accessibility: { + ariaLabels: 22, + contrastRatio: "4.8:1", + keyboardNav: true, + altTexts: 25, + headingStructure: true, + }, + performance: { + firstContentfulPaint: "1.4s", + speedIndex: "2.9s", + timeToInteractive: "3.5s", + totalBlockingTime: "250ms", + largestContentfulPaint: "2.8s", + cumulativeLayoutShift: "0.12", + }, + bestPractices: { + bestPractices: 92, + }, + }, + coreWebVitals: { + LCP: "2.8s", + FID: "120ms", + CLS: "0.12", + }, + recommendations: [ + { + title: "Improve accessibility standards", + description: + "Enhance color contrast ratios and ARIA implementation", + impact: "high", + category: "accessibility", + }, + { + title: "Optimize Core Web Vitals", + description: + "Reduce Largest Contentful Paint and improve CLS scores", + impact: "high", + category: "performance", + }, + { + title: "Update best practices", + description: + "Implement security headers and update deprecated APIs", + impact: "medium", + category: "best-practices", + }, + { + title: "Mobile responsiveness", + description: "Enhance mobile layout and touch target sizes", + impact: "medium", + category: "accessibility", + }, + ], + }, + ], + }), + + update: (prevData: CompetitorData): CompetitorData => { + const updateValue = (value: number) => { + const change = Math.random() * 4 - 2; + return Math.min(100, Math.max(0, value + change)); + }; + + return { + ...prevData, + yourSite: { + ...prevData.yourSite, + performance: updateValue(prevData.yourSite.performance), + seo: updateValue(prevData.yourSite.seo), + accessibility: updateValue(prevData.yourSite.accessibility), + bestPractices: updateValue(prevData.yourSite.bestPractices), + }, + competitors: prevData.competitors.map((competitor) => ({ + ...competitor, + performance: updateValue(competitor.performance), + seo: updateValue(competitor.seo), + accessibility: updateValue(competitor.accessibility), + bestPractices: updateValue(competitor.bestPractices), + })), + }; + }, + }, + + formatters: { + metricValue: ( + value: number, + type: "time" | "size" | "percentage" | "default", + ): string => { + switch (type) { + case "time": + return `${value.toFixed(2)}s`; + case "size": + return `${(value / 1024).toFixed(2)}KB`; + case "percentage": + return `${(value * 100).toFixed(1)}%`; + default: + return value.toString(); + } + }, + + severity: ( + value: number, + threshold: { good: number; moderate: number }, + ): "good" | "moderate" | "poor" => { + if (value >= threshold.good) return "good"; + if (value >= threshold.moderate) return "moderate"; + return "poor"; + }, + }, +}; + +export const generateDashboardData = dataGenerators.dashboard.generate; +export const updateMetrics = dataGenerators.dashboard.update; +export const generateCompetitorData = dataGenerators.competitor.generate; +export const updateCompetitorMetrics = dataGenerators.competitor.update; diff --git a/website-monitoring-frontend/src/utils/errorUtils.ts b/website-monitoring-frontend/src/utils/errorUtils.ts new file mode 100644 index 0000000..614a2fc --- /dev/null +++ b/website-monitoring-frontend/src/utils/errorUtils.ts @@ -0,0 +1,278 @@ +/** + * Utility functions for proper error handling and logging + */ + +/** + * Safely serialize an error object to avoid circular references + * and ensure all properties are captured + */ +export function serializeError(error: unknown): string { + if (error instanceof Error) { + const errorObj: Record<string, unknown> = { + name: error.name, + message: error.message, + stack: error.stack, + cause: error.cause, + }; + + // Handle Supabase errors specifically + if (error.name === 'PostgrestError' || error.message.includes('PostgrestError')) { + try { + const supabaseError = error as any; + errorObj.details = supabaseError.details; + errorObj.hint = supabaseError.hint; + errorObj.code = supabaseError.code; + } catch { + // Ignore if we can't extract Supabase-specific properties + } + } + + // Include any additional properties + try { + const additionalProps = Object.getOwnPropertyNames(error).reduce((acc, key) => { + if (!['name', 'message', 'stack', 'cause'].includes(key)) { + try { + const value = (error as any)[key]; + if (value !== undefined && value !== null) { + acc[key] = value; + } + } catch { + acc[key] = '[Circular Reference]'; + } + } + return acc; + }, {} as Record<string, unknown>); + + Object.assign(errorObj, additionalProps); + } catch { + // If we can't extract additional properties, continue with basic info + } + + try { + return JSON.stringify(errorObj, null, 2); + } catch { + return `Error: ${error.name} - ${error.message}`; + } + } + + if (typeof error === 'object' && error !== null) { + try { + // Handle objects that might be error-like + const errorLike = error as Record<string, unknown>; + if (errorLike.message || errorLike.error || errorLike.details) { + return JSON.stringify({ + message: errorLike.message, + error: errorLike.error, + details: errorLike.details, + code: errorLike.code, + hint: errorLike.hint, + ...Object.keys(errorLike).reduce((acc, key) => { + if (!['message', 'error', 'details', 'code', 'hint'].includes(key)) { + try { + acc[key] = errorLike[key]; + } catch { + acc[key] = '[Non-serializable]'; + } + } + return acc; + }, {} as Record<string, unknown>) + }, null, 2); + } + + return JSON.stringify(error, null, 2); + } catch { + return '[Non-serializable object]'; + } + } + + return String(error); +} + +/** + * Enhanced console.error that properly serializes errors + */ +export function logError(message: string, error: unknown, context?: Record<string, any>): void { + // Try to extract more information from the error + let errorDetails = serializeError(error); + + // If the error is still showing as an object, try to extract more info + if (errorDetails === '[Non-serializable object]' || errorDetails.includes('Object')) { + try { + if (error && typeof error === 'object') { + const errorObj = error as Record<string, unknown>; + const extractedInfo: Record<string, unknown> = {}; + + // Try to extract common error properties + ['message', 'error', 'details', 'code', 'hint', 'status', 'statusText', 'response'].forEach(key => { + if (errorObj[key] !== undefined) { + extractedInfo[key] = errorObj[key]; + } + }); + + // If we found some properties, use them + if (Object.keys(extractedInfo).length > 0) { + errorDetails = JSON.stringify(extractedInfo, null, 2); + } else { + // Try to stringify the entire object with a replacer + errorDetails = JSON.stringify(errorObj, (key, value) => { + if (typeof value === 'function') return '[Function]'; + if (value === undefined) return '[Undefined]'; + return value; + }, 2); + } + } + } catch { + errorDetails = `[Error object: ${error?.constructor?.name || 'Unknown'}]`; + } + } + + const errorInfo = { + message, + error: errorDetails, + context, + timestamp: new Date().toISOString(), + userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'server', + errorType: error?.constructor?.name || typeof error, + }; + + console.error('🚨 Error:', errorInfo); + + // In development, also log the original error for better debugging + if (process.env.NODE_ENV === 'development') { + console.error('Original error:', error); + if (error && typeof error === 'object') { + console.error('Error keys:', Object.keys(error)); + console.error('Error prototype:', Object.getPrototypeOf(error)); + } + } +} + +/** + * Create a user-friendly error message from an error object + */ +export function getUserFriendlyErrorMessage(error: unknown): string { + if (error instanceof Error) { + // Handle common error types + if (error.message.includes('does not exist')) { + return 'Database table not found. Please check your database setup.'; + } + if (error.message.includes('network') || error.message.includes('fetch')) { + return 'Network error. Please check your internet connection and try again.'; + } + if (error.message.includes('unauthorized') || error.message.includes('401')) { + return 'Authentication required. Please log in again.'; + } + if (error.message.includes('forbidden') || error.message.includes('403')) { + return 'Access denied. You don\'t have permission to perform this action.'; + } + if (error.message.includes('not found') || error.message.includes('404')) { + return 'Resource not found. The requested data is not available.'; + } + if (error.message.includes('timeout')) { + return 'Request timed out. Please try again.'; + } + + return error.message; + } + + if (typeof error === 'string') { + return error; + } + + return 'An unexpected error occurred. Please try again.'; +} + +/** + * Check if an error is related to database connection issues + */ +export function isDatabaseError(error: unknown): boolean { + if (error instanceof Error) { + const message = error.message.toLowerCase(); + return message.includes('database') || + message.includes('connection') || + message.includes('supabase') || + message.includes('does not exist') || + message.includes('relation') || + message.includes('table'); + } + return false; +} + +/** + * Check if an error is related to network issues + */ +export function isNetworkError(error: unknown): boolean { + if (error instanceof Error) { + const message = error.message.toLowerCase(); + return message.includes('network') || + message.includes('fetch') || + message.includes('timeout') || + message.includes('connection refused'); + } + return false; +} + +/** + * Check if an error is related to authentication issues + */ +export function isAuthError(error: unknown): boolean { + if (error instanceof Error) { + const message = error.message.toLowerCase(); + return message.includes('unauthorized') || + message.includes('401') || + message.includes('forbidden') || + message.includes('403') || + message.includes('authentication'); + } + return false; +} + +/** + * Extract detailed information from Supabase errors + */ +export function extractSupabaseErrorInfo(error: unknown): { + message: string; + details?: string; + hint?: string; + code?: string; + status?: number; +} { + if (error instanceof Error) { + const result: { + message: string; + details?: string; + hint?: string; + code?: string; + status?: number; + } = { + message: error.message, + }; + + try { + const errorObj = error as any; + if (errorObj.details) result.details = errorObj.details; + if (errorObj.hint) result.hint = errorObj.hint; + if (errorObj.code) result.code = errorObj.code; + if (errorObj.status) result.status = errorObj.status; + } catch { + // Ignore if we can't extract additional properties + } + + return result; + } + + if (typeof error === 'object' && error !== null) { + const errorObj = error as Record<string, unknown>; + return { + message: String(errorObj.message || errorObj.error || 'Unknown error'), + details: errorObj.details as string, + hint: errorObj.hint as string, + code: errorObj.code as string, + status: errorObj.status as number, + }; + } + + return { + message: String(error), + }; +} \ No newline at end of file diff --git a/website-monitoring-frontend/src/utils/htmlUtils.ts b/website-monitoring-frontend/src/utils/htmlUtils.ts new file mode 100644 index 0000000..dde3e7d --- /dev/null +++ b/website-monitoring-frontend/src/utils/htmlUtils.ts @@ -0,0 +1,66 @@ +// Utility functions for handling HTML entities and text processing + +/** + * Decodes HTML entities in a string + * @param text - The text containing HTML entities + * @returns The decoded text + */ +export function decodeHtmlEntities(text: string): string { + if (!text) return text; + + // Create a temporary element to decode HTML entities + const textarea = document.createElement('textarea'); + textarea.innerHTML = text; + return textarea.value; +} + +/** + * Safely decodes HTML entities (works in both browser and server environments) + * @param text - The text containing HTML entities + * @returns The decoded text + */ +export function safeDecodeHtmlEntities(text: string): string { + if (!text) return text; + + // Common HTML entities mapping + const htmlEntities: { [key: string]: string } = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + '/': '/', + '`': '`', + '=': '=', + ''': "'", + ' ': ' ', + '©': '©', + '®': '®', + '™': '™', + '…': '…', + '—': '—', + '–': '–', + '‘': '\u2018', + '’': '\u2019', + '“': '\u201C', + '”': '\u201D', + }; + + // Replace HTML entities + let decodedText = text; + for (const [entity, replacement] of Object.entries(htmlEntities)) { + decodedText = decodedText.replace(new RegExp(entity, 'g'), replacement); + } + + // Handle numeric HTML entities (', ', etc.) + decodedText = decodedText.replace(/&#x([0-9a-fA-F]+);/g, (match, hex) => { + return String.fromCharCode(parseInt(hex, 16)); + }); + + // Handle decimal HTML entities (', etc.) + decodedText = decodedText.replace(/&#(\d+);/g, (match, decimal) => { + return String.fromCharCode(parseInt(decimal, 10)); + }); + + return decodedText; +} diff --git a/website-monitoring-frontend/supabase-fixes.sql b/website-monitoring-frontend/supabase-fixes.sql new file mode 100644 index 0000000..9fe561a --- /dev/null +++ b/website-monitoring-frontend/supabase-fixes.sql @@ -0,0 +1,228 @@ +-- Supabase Database Fixes for Website Monitoring Frontend +-- Run this in your Supabase SQL editor + +-- 0. Create missing enum types if they don't exist +DO $$ BEGIN + CREATE TYPE scan_status AS ENUM ( + 'pending', + 'queued', + 'running', + 'completed', + 'failed', + 'cancelled' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE severity_level AS ENUM ( + 'critical', + 'high', + 'medium', + 'low', + 'info' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE comparison_operator AS ENUM ( + 'less_than', + 'less_than_equal', + 'greater_than', + 'greater_than_equal', + 'equal_to', + 'not_equal_to' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE metric_category AS ENUM ( + 'performance', + 'seo', + 'accessibility', + 'best_practices', + 'security', + 'pwa' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE resource_type AS ENUM ( + 'script', + 'stylesheet', + 'image', + 'font', + 'document', + 'media', + 'other' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE notification_channel AS ENUM ( + 'email', + 'slack', + 'webhook', + 'in_app' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE subscription_tier AS ENUM ( + 'free', + 'starter', + 'professional', + 'enterprise' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE user_role AS ENUM ( + 'owner', + 'admin', + 'editor', + 'viewer' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- 1. Add missing columns to scans table +ALTER TABLE scans ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMPTZ; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS trigger_type VARCHAR DEFAULT 'manual'; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS website_id UUID REFERENCES websites(id); +ALTER TABLE scans ADD COLUMN IF NOT EXISTS triggered_by UUID REFERENCES users(id); +ALTER TABLE scans ADD COLUMN IF NOT EXISTS scan_type VARCHAR DEFAULT 'full'; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS priority INTEGER DEFAULT 1; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS categories TEXT[] DEFAULT ARRAY['performance', 'seo', 'accessibility', 'best_practices']; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS device_type VARCHAR DEFAULT 'desktop'; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS user_agent VARCHAR; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS lighthouse_version VARCHAR; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS chrome_version VARCHAR; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS environment JSONB DEFAULT '{}'::jsonb; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS started_at TIMESTAMPTZ DEFAULT NOW(); +ALTER TABLE scans ADD COLUMN IF NOT EXISTS completed_at TIMESTAMPTZ; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS duration_ms INTEGER; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS error_message TEXT; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS retry_count INTEGER DEFAULT 0; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(); + +-- 2. Add missing columns to scan_results table if they don't exist +ALTER TABLE scan_results ADD COLUMN IF NOT EXISTS raw_data JSONB DEFAULT '{}'::jsonb; + +-- 3. Add missing columns to metric_values table if they don't exist +ALTER TABLE metric_values ADD COLUMN IF NOT EXISTS raw_value VARCHAR; +ALTER TABLE metric_values ADD COLUMN IF NOT EXISTS unit VARCHAR; +ALTER TABLE metric_values ADD COLUMN IF NOT EXISTS is_passing BOOLEAN; + +-- 4. Add missing columns to resource_analysis table if they don't exist +ALTER TABLE resource_analysis ADD COLUMN IF NOT EXISTS transfer_size_bytes INTEGER; +ALTER TABLE resource_analysis ADD COLUMN IF NOT EXISTS duration_ms INTEGER; +ALTER TABLE resource_analysis ADD COLUMN IF NOT EXISTS is_third_party BOOLEAN DEFAULT false; +ALTER TABLE resource_analysis ADD COLUMN IF NOT EXISTS is_cached BOOLEAN; +ALTER TABLE resource_analysis ADD COLUMN IF NOT EXISTS compression_ratio NUMERIC; +ALTER TABLE resource_analysis ADD COLUMN IF NOT EXISTS mime_type VARCHAR; +ALTER TABLE resource_analysis ADD COLUMN IF NOT EXISTS protocol VARCHAR; +ALTER TABLE resource_analysis ADD COLUMN IF NOT EXISTS priority VARCHAR; +ALTER TABLE resource_analysis ADD COLUMN IF NOT EXISTS status_code INTEGER; +ALTER TABLE resource_analysis ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb; + +-- 5. Add missing columns to alert_configurations table if they don't exist +ALTER TABLE alert_configurations ADD COLUMN IF NOT EXISTS consecutive_count INTEGER DEFAULT 1; +ALTER TABLE alert_configurations ADD COLUMN IF NOT EXISTS cooldown_minutes INTEGER DEFAULT 60; +ALTER TABLE alert_configurations ADD COLUMN IF NOT EXISTS notification_template TEXT; +ALTER TABLE alert_configurations ADD COLUMN IF NOT EXISTS last_triggered_at TIMESTAMPTZ; + +-- 6. Add missing columns to alerts table if they don't exist +ALTER TABLE alerts ADD COLUMN IF NOT EXISTS details JSONB DEFAULT '{}'::jsonb; +ALTER TABLE alerts ADD COLUMN IF NOT EXISTS acknowledged_by UUID REFERENCES users(id); +ALTER TABLE alerts ADD COLUMN IF NOT EXISTS acknowledged_at TIMESTAMPTZ; +ALTER TABLE alerts ADD COLUMN IF NOT EXISTS resolved_at TIMESTAMPTZ; + +-- 7. Add missing columns to crawl_queue table if they don't exist +ALTER TABLE crawl_queue ADD COLUMN IF NOT EXISTS discovery_depth INTEGER DEFAULT 0; +ALTER TABLE crawl_queue ADD COLUMN IF NOT EXISTS attempts INTEGER DEFAULT 0; +ALTER TABLE crawl_queue ADD COLUMN IF NOT EXISTS parent_url VARCHAR; +ALTER TABLE crawl_queue ADD COLUMN IF NOT EXISTS priority INTEGER DEFAULT 1; +ALTER TABLE crawl_queue ADD COLUMN IF NOT EXISTS error_message TEXT; +ALTER TABLE crawl_queue ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb; + +-- 8. Add missing columns to crawl_sessions table if they don't exist +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS pages_discovered INTEGER DEFAULT 0; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS pages_processed INTEGER DEFAULT 0; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS current_url VARCHAR; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS error_message TEXT; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb; + +-- 9. Create indexes if they don't exist +CREATE INDEX IF NOT EXISTS idx_scans_scheduled_at ON scans(scheduled_at); +CREATE INDEX IF NOT EXISTS idx_scan_results_category ON scan_results(category); +CREATE INDEX IF NOT EXISTS idx_metric_values_created_at ON metric_values(created_at); +CREATE INDEX IF NOT EXISTS idx_resource_analysis_type ON resource_analysis(resource_type); +CREATE INDEX IF NOT EXISTS idx_alerts_status ON alerts(status); +CREATE INDEX IF NOT EXISTS idx_crawl_queue_status_priority ON crawl_queue(status, priority); +CREATE INDEX IF NOT EXISTS idx_crawl_sessions_status ON crawl_sessions(status); + +-- 10. Add RLS policies if they don't exist (basic ones) +-- Note: You may need to customize these based on your security requirements + +-- Enable RLS on all tables +ALTER TABLE scans ENABLE ROW LEVEL SECURITY; +ALTER TABLE scan_results ENABLE ROW LEVEL SECURITY; +ALTER TABLE metric_values ENABLE ROW LEVEL SECURITY; +ALTER TABLE resource_analysis ENABLE ROW LEVEL SECURITY; +ALTER TABLE alert_configurations ENABLE ROW LEVEL SECURITY; +ALTER TABLE alerts ENABLE ROW LEVEL SECURITY; +ALTER TABLE crawl_queue ENABLE ROW LEVEL SECURITY; +ALTER TABLE crawl_sessions ENABLE ROW LEVEL SECURITY; + +-- Basic RLS policies (you may want to customize these) +CREATE POLICY IF NOT EXISTS "Users can view scans for their organization" ON scans + FOR SELECT USING ( + website_id IN ( + SELECT id FROM websites WHERE organization_id IN ( + SELECT organization_id FROM users WHERE id = auth.uid() + ) + ) + ); + +CREATE POLICY IF NOT EXISTS "Users can insert scans for their organization" ON scans + FOR INSERT WITH CHECK ( + website_id IN ( + SELECT id FROM websites WHERE organization_id IN ( + SELECT organization_id FROM users WHERE id = auth.uid() + ) + ) + ); + +CREATE POLICY IF NOT EXISTS "Users can update scans for their organization" ON scans + FOR UPDATE USING ( + website_id IN ( + SELECT id FROM websites WHERE organization_id IN ( + SELECT organization_id FROM users WHERE id = auth.uid() + ) + ) + ); + +-- Similar policies for other tables... +-- (You may want to add more comprehensive RLS policies based on your needs) + +-- 11. Refresh Supabase schema cache to pick up new columns +-- This is important to resolve "Could not find column in schema cache" errors +NOTIFY pgrst, 'reload schema'; + +COMMIT; \ No newline at end of file diff --git a/website-monitoring-frontend/supabase/.gitignore b/website-monitoring-frontend/supabase/.gitignore new file mode 100644 index 0000000..ad9264f --- /dev/null +++ b/website-monitoring-frontend/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/website-monitoring-frontend/supabase/config.toml b/website-monitoring-frontend/supabase/config.toml new file mode 100644 index 0000000..4509758 --- /dev/null +++ b/website-monitoring-frontend/supabase/config.toml @@ -0,0 +1,334 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "website-monitoring-frontend" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions://<database>/<schema>/<hook_name>" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +[edge_runtime] +enabled = true +# Configure one of the supported request policies: `oneshot`, `per_worker`. +# Use `oneshot` for hot reload, or `per_worker` for load testing. +policy = "oneshot" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 1 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/website-monitoring-frontend/supabase/migrations/20250813102225_remote_schema.sql b/website-monitoring-frontend/supabase/migrations/20250813102225_remote_schema.sql new file mode 100644 index 0000000..0e78ce8 --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102225_remote_schema.sql @@ -0,0 +1,2453 @@ + + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + + +CREATE EXTENSION IF NOT EXISTS "pgsodium"; + + + + + + +COMMENT ON SCHEMA "public" IS 'standard public schema'; + + + +CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pg_trgm" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions"; + + + + + + +CREATE TYPE "public"."comparison_operator" AS ENUM ( + 'less_than', + 'less_than_equal', + 'greater_than', + 'greater_than_equal', + 'equal_to', + 'not_equal_to' +); + + +ALTER TYPE "public"."comparison_operator" OWNER TO "postgres"; + + +CREATE TYPE "public"."metric_category" AS ENUM ( + 'performance', + 'seo', + 'accessibility', + 'best_practices', + 'security', + 'pwa' +); + + +ALTER TYPE "public"."metric_category" OWNER TO "postgres"; + + +CREATE TYPE "public"."notification_channel" AS ENUM ( + 'email', + 'slack', + 'webhook', + 'in_app' +); + + +ALTER TYPE "public"."notification_channel" OWNER TO "postgres"; + + +CREATE TYPE "public"."resource_type" AS ENUM ( + 'script', + 'stylesheet', + 'image', + 'font', + 'document', + 'media', + 'other' +); + + +ALTER TYPE "public"."resource_type" OWNER TO "postgres"; + + +CREATE TYPE "public"."scan_status" AS ENUM ( + 'pending', + 'queued', + 'running', + 'completed', + 'failed', + 'cancelled' +); + + +ALTER TYPE "public"."scan_status" OWNER TO "postgres"; + + +CREATE TYPE "public"."severity_level" AS ENUM ( + 'critical', + 'high', + 'medium', + 'low', + 'info' +); + + +ALTER TYPE "public"."severity_level" OWNER TO "postgres"; + + +CREATE TYPE "public"."subscription_tier" AS ENUM ( + 'free', + 'starter', + 'professional', + 'enterprise' +); + + +ALTER TYPE "public"."subscription_tier" OWNER TO "postgres"; + + +CREATE TYPE "public"."user_role" AS ENUM ( + 'owner', + 'admin', + 'editor', + 'viewer' +); + + +ALTER TYPE "public"."user_role" OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."apply_data_retention"() RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' + AS $$ +BEGIN + -- Delete old scan results (older than 90 days) + DELETE FROM public.scan_results + WHERE created_at < NOW() - INTERVAL '90 days'; + + -- Delete old metric values (older than 90 days) + DELETE FROM public.metric_values + WHERE created_at < NOW() - INTERVAL '90 days'; + + -- Delete old alert history (older than 365 days) + DELETE FROM public.alert_history + WHERE created_at < NOW() - INTERVAL '365 days'; + + -- Delete old audit logs (older than 365 days) + DELETE FROM public.audit_logs + WHERE created_at < NOW() - INTERVAL '365 days'; +END; +$$; + + +ALTER FUNCTION "public"."apply_data_retention"() OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."calculate_competitor_comparison"("website_id" "uuid") RETURNS TABLE("metric_key" character varying, "your_score" numeric, "competitor_avg" numeric, "competitor_best" numeric, "percentile" numeric) + LANGUAGE "plpgsql" + SET "search_path" TO 'public', 'pg_temp' + AS $$ +BEGIN + RETURN QUERY + WITH your_metrics AS ( + SELECT + m.key, + mv.value + FROM scans s + JOIN metric_values mv ON mv.scan_id = s.id + JOIN metric_definitions m ON m.id = mv.metric_id + WHERE s.website_id = calculate_competitor_comparison.website_id + AND s.created_at = ( + SELECT MAX(created_at) + FROM scans + WHERE website_id = calculate_competitor_comparison.website_id + ) + ), + competitor_metrics AS ( + SELECT + m.key, + mv.value, + PERCENT_RANK() OVER (PARTITION BY m.key ORDER BY mv.value) as percentile + FROM scans s + JOIN metric_values mv ON mv.scan_id = s.id + JOIN metric_definitions m ON m.id = mv.metric_id + WHERE s.website_id IN ( + SELECT competitor_url_id + FROM competitor_websites + WHERE website_id = calculate_competitor_comparison.website_id + ) + AND s.created_at >= NOW() - INTERVAL '30 days' + ) + SELECT + ym.key, + ym.value as your_score, + AVG(cm.value) as competitor_avg, + MAX(cm.value) as competitor_best, + MAX(cm.percentile) * 100 as percentile + FROM your_metrics ym + LEFT JOIN competitor_metrics cm ON cm.key = ym.key + GROUP BY ym.key, ym.value; +END; +$$; + + +ALTER FUNCTION "public"."calculate_competitor_comparison"("website_id" "uuid") OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."calculate_competitor_comparison"("website_id_param" "uuid", "competitor_id_param" "uuid") RETURNS "jsonb" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' + AS $$ +DECLARE + result jsonb; +BEGIN + -- Calculate comparison metrics between website and competitor + SELECT jsonb_build_object( + 'performance_score_diff', + COALESCE(w.performance_score - c.performance_score, 0), + 'seo_score_diff', + COALESCE(w.seo_score - c.seo_score, 0), + 'accessibility_score_diff', + COALESCE(w.accessibility_score - c.accessibility_score, 0) + ) INTO result + FROM public.websites w + CROSS JOIN public.competitor_websites c + WHERE w.id = website_id_param + AND c.id = competitor_id_param; + + RETURN COALESCE(result, '{}'::jsonb); +END; +$$; + + +ALTER FUNCTION "public"."calculate_competitor_comparison"("website_id_param" "uuid", "competitor_id_param" "uuid") OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."calculate_health_score"("website_id_param" "uuid") RETURNS numeric + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' + AS $$ +DECLARE + health_score numeric; +BEGIN + -- Calculate weighted health score based on latest metrics + SELECT + COALESCE( + (performance_score * 0.4 + + seo_score * 0.3 + + accessibility_score * 0.2 + + best_practices_score * 0.1), + 0 + ) + INTO health_score + FROM public.websites + WHERE id = website_id_param; + + RETURN COALESCE(health_score, 0); +END; +$$; + + +ALTER FUNCTION "public"."calculate_health_score"("website_id_param" "uuid") OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."email_exists"("email_to_check" "text") RETURNS boolean + LANGUAGE "sql" SECURITY DEFINER + AS $$ + select exists( + select 1 from users where lower(email) = lower(email_to_check) + ); +$$; + + +ALTER FUNCTION "public"."email_exists"("email_to_check" "text") OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."generate_performance_report"("website_id" "uuid", "days" integer) RETURNS "jsonb" + LANGUAGE "plpgsql" + SET "search_path" TO 'public', 'pg_temp' + AS $$ +DECLARE + report JSONB; +BEGIN + SELECT jsonb_build_object( + 'website_info', ( + SELECT jsonb_build_object( + 'name', name, + 'url', base_url, + 'report_period', jsonb_build_object( + 'start', NOW() - (days || ' days')::interval, + 'end', NOW() + ) + ) + FROM websites + WHERE id = website_id + ), + 'performance_summary', ( + SELECT jsonb_build_object( + 'average_performance_score', AVG(mv.value), + 'best_performance_score', MAX(mv.value), + 'worst_performance_score', MIN(mv.value), + 'trend', jsonb_agg( + jsonb_build_object( + 'date', DATE(s.created_at), + 'score', mv.value + ) ORDER BY s.created_at + ) + ) + FROM scans s + JOIN metric_values mv ON mv.scan_id = s.id + JOIN metric_definitions md ON md.id = mv.metric_id + WHERE s.website_id = generate_performance_report.website_id + AND md.key = 'performance' + AND s.created_at >= NOW() - (days || ' days')::interval + ), + 'core_metrics', ( + SELECT jsonb_object_agg( + md.key, + jsonb_build_object( + 'average', AVG(mv.value), + 'best', MAX(mv.value), + 'worst', MIN(mv.value) + ) + ) + FROM scans s + JOIN metric_values mv ON mv.scan_id = s.id + JOIN metric_definitions md ON md.id = mv.metric_id + WHERE s.website_id = generate_performance_report.website_id + AND md.is_core_metric = true + AND s.created_at >= NOW() - (days || ' days')::interval + ), + 'resource_summary', ( + SELECT jsonb_object_agg( + resource_type, + jsonb_build_object( + 'count', COUNT(*), + 'total_size', SUM(size_bytes), + 'average_duration', AVG(duration_ms) + ) + ) + FROM scans s + JOIN resource_analysis ra ON ra.scan_id = s.id + WHERE s.website_id = generate_performance_report.website_id + AND s.created_at >= NOW() - (days || ' days')::interval + ), + 'alerts', ( + SELECT jsonb_agg( + jsonb_build_object( + 'severity', severity, + 'message', message, + 'created_at', created_at + ) + ) + FROM alerts + WHERE website_id = generate_performance_report.website_id + AND created_at >= NOW() - (days || ' days')::interval + ) + ) INTO report; + + RETURN report; +END; +$$; + + +ALTER FUNCTION "public"."generate_performance_report"("website_id" "uuid", "days" integer) OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."generate_performance_report"("website_id_param" "uuid", "start_date" timestamp with time zone DEFAULT ("now"() - '30 days'::interval), "end_date" timestamp with time zone DEFAULT "now"()) RETURNS "jsonb" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' + AS $$ +DECLARE + report jsonb; +BEGIN + -- Generate comprehensive performance report + SELECT jsonb_build_object( + 'website_id', website_id_param, + 'period', jsonb_build_object( + 'start_date', start_date, + 'end_date', end_date + ), + 'summary', jsonb_build_object( + 'total_scans', COUNT(s.id), + 'avg_performance_score', AVG(w.performance_score), + 'avg_seo_score', AVG(w.seo_score), + 'avg_accessibility_score', AVG(w.accessibility_score) + ), + 'trends', jsonb_agg( + jsonb_build_object( + 'date', DATE(s.created_at), + 'performance_score', w.performance_score, + 'seo_score', w.seo_score + ) + ) + ) INTO report + FROM public.scans s + JOIN public.websites w ON w.id = s.website_id + WHERE s.website_id = website_id_param + AND s.created_at BETWEEN start_date AND end_date + GROUP BY website_id_param; + + RETURN COALESCE(report, '{}'::jsonb); +END; +$$; + + +ALTER FUNCTION "public"."generate_performance_report"("website_id_param" "uuid", "start_date" timestamp with time zone, "end_date" timestamp with time zone) OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."handle_new_user"() RETURNS "trigger" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' + AS $$ +BEGIN + -- Log the new row to Postgres logs for debugging + RAISE LOG 'New user created: %', ROW(NEW.*); + + -- Insert into public.users table + INSERT INTO public.users (id, email, organization_id, role, created_at, updated_at) + VALUES ( + NEW.id, + NEW.email, + NEW.raw_user_meta_data->>'organization_id', + COALESCE(NEW.raw_user_meta_data->>'role', 'member'), + NOW(), + NOW() + ); + + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."handle_new_user"() OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."perform_maintenance"() RETURNS "void" + LANGUAGE "plpgsql" + SET "search_path" TO 'public', 'pg_temp' + AS $$ +BEGIN + -- Clean up old rate limit records + DELETE FROM rate_limits + WHERE window_start < NOW() - INTERVAL '1 day'; + + -- Archive old notifications + UPDATE notification_deliveries + SET status = 'archived' + WHERE created_at < NOW() - INTERVAL '30 days'; + + -- Clean up expired API keys + UPDATE api_keys + SET is_active = false + WHERE expires_at < NOW(); + + -- Update statistics + ANALYZE websites; + ANALYZE scans; + ANALYZE metric_values; + ANALYZE resource_analysis; + + -- Vacuum analyze for better query planning + VACUUM ANALYZE websites; + VACUUM ANALYZE scans; + VACUUM ANALYZE metric_values; + VACUUM ANALYZE resource_analysis; +END; +$$; + + +ALTER FUNCTION "public"."perform_maintenance"() OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."process_alert_notifications"() RETURNS "trigger" + LANGUAGE "plpgsql" + SET "search_path" TO 'public', 'pg_temp' + AS $$ +DECLARE + website_record RECORD; + user_record RECORD; +BEGIN + -- Get website details + SELECT * INTO website_record + FROM websites + WHERE id = NEW.website_id; + + -- Insert notification for each user in the organization + FOR user_record IN + SELECT u.* + FROM users u + WHERE u.organization_id = website_record.organization_id + AND (u.settings->>'email_notifications')::boolean = true + LOOP + INSERT INTO notification_deliveries ( + alert_id, + channel, + recipient, + content + ) VALUES ( + NEW.id, + 'email', + user_record.email, + format( + 'Alert for %s: %s. Severity: %s', + website_record.name, + NEW.message, + NEW.severity + ) + ); + END LOOP; + + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."process_alert_notifications"() OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."update_scan_status"() RETURNS "trigger" + LANGUAGE "plpgsql" + SET "search_path" TO 'public', 'pg_temp' + AS $$ +BEGIN + IF NEW.status = 'completed' THEN + -- Update website's last scan timestamp + UPDATE websites + SET last_scan_at = NOW() + WHERE id = NEW.website_id; + + -- Check for alerts + INSERT INTO alerts (website_id, page_id, severity, title, message) + SELECT + NEW.website_id, + NEW.page_id, + ac.severity, + 'Metric threshold exceeded', + format('%s is %s threshold of %s', m.name, ac.comparison, ac.threshold) + FROM metric_values mv + JOIN metric_definitions m ON m.id = mv.metric_id + JOIN alert_configurations ac ON ac.metric_id = m.id + WHERE mv.scan_id = NEW.id + AND ac.website_id = NEW.website_id + AND ( + CASE ac.comparison + WHEN 'less_than' THEN mv.value < ac.threshold + WHEN 'greater_than' THEN mv.value > ac.threshold + WHEN 'equal_to' THEN mv.value = ac.threshold + END + ); + END IF; + + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."update_scan_status"() OWNER TO "postgres"; + +SET default_tablespace = ''; + +SET default_table_access_method = "heap"; + + +CREATE TABLE IF NOT EXISTS "public"."alert_configurations" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "website_id" "uuid" NOT NULL, + "name" character varying NOT NULL, + "description" "text", + "metric_id" "uuid" NOT NULL, + "threshold" numeric NOT NULL, + "comparison" "public"."comparison_operator" DEFAULT 'less_than'::"public"."comparison_operator", + "severity" "public"."severity_level" DEFAULT 'medium'::"public"."severity_level", + "consecutive_count" integer DEFAULT 1, + "cooldown_minutes" integer DEFAULT 60, + "notification_channels" "public"."notification_channel"[] DEFAULT ARRAY['email'::"public"."notification_channel"], + "notification_template" "text", + "is_active" boolean DEFAULT true, + "last_triggered_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."alert_configurations" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."alert_history" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "alert_id" "uuid" NOT NULL, + "event_type" character varying NOT NULL, + "event_data" "jsonb" NOT NULL, + "created_by" "uuid", + "created_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."alert_history" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."alerts" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "website_id" "uuid" NOT NULL, + "page_id" "uuid", + "config_id" "uuid", + "metric_id" "uuid", + "severity" "public"."severity_level" DEFAULT 'medium'::"public"."severity_level", + "title" character varying NOT NULL, + "message" "text" NOT NULL, + "details" "jsonb" DEFAULT '{}'::"jsonb", + "status" character varying DEFAULT 'open'::character varying, + "acknowledged_by" "uuid", + "acknowledged_at" timestamp with time zone, + "resolved_at" timestamp with time zone, + "resolution_note" "text", + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."alerts" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."api_keys" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "organization_id" "uuid" NOT NULL, + "name" character varying NOT NULL, + "key_hash" character varying NOT NULL, + "scopes" character varying[] DEFAULT ARRAY['read'::"text"], + "rate_limit_per_minute" integer DEFAULT 60, + "is_active" boolean DEFAULT true, + "last_used_at" timestamp with time zone, + "expires_at" timestamp with time zone, + "created_by" "uuid", + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."api_keys" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."audit_logs" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "organization_id" "uuid", + "user_id" "uuid", + "action" character varying NOT NULL, + "entity_type" character varying NOT NULL, + "entity_id" "uuid", + "changes" "jsonb", + "ip_address" character varying, + "user_agent" character varying, + "created_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."audit_logs" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."budget_violations" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "budget_id" "uuid" NOT NULL, + "scan_id" "uuid" NOT NULL, + "actual_value" numeric NOT NULL, + "threshold_value" numeric NOT NULL, + "percentage_over" numeric, + "created_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."budget_violations" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."competitor_websites" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "website_id" "uuid" NOT NULL, + "competitor_url" character varying NOT NULL, + "name" character varying NOT NULL, + "is_active" boolean DEFAULT true, + "scan_frequency" character varying DEFAULT 'daily'::character varying, + "metrics_to_track" character varying[] DEFAULT ARRAY['performance'::"text", 'seo'::"text", 'accessibility'::"text", 'best_practices'::"text"], + "last_scan_at" timestamp with time zone, + "metadata" "jsonb" DEFAULT '{}'::"jsonb", + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."competitor_websites" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."crawl_queue" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "website_id" "uuid" NOT NULL, + "url" character varying NOT NULL, + "priority" integer DEFAULT 1, + "status" character varying DEFAULT 'pending'::character varying, + "parent_url" character varying, + "discovery_depth" integer DEFAULT 0, + "attempts" integer DEFAULT 0, + "last_attempt_at" timestamp with time zone, + "next_attempt_at" timestamp with time zone, + "error_message" "text", + "metadata" "jsonb" DEFAULT '{}'::"jsonb", + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."crawl_queue" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."crawl_sessions" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "website_id" "uuid" NOT NULL, + "status" character varying DEFAULT 'running'::character varying, + "pages_discovered" integer DEFAULT 0, + "pages_processed" integer DEFAULT 0, + "start_url" character varying NOT NULL, + "max_depth" integer, + "started_at" timestamp with time zone DEFAULT "now"(), + "completed_at" timestamp with time zone, + "error_message" "text", + "metadata" "jsonb" DEFAULT '{}'::"jsonb", + "created_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."crawl_sessions" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."dashboard_widgets" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "dashboard_id" "uuid" NOT NULL, + "widget_type" character varying NOT NULL, + "name" character varying NOT NULL, + "config" "jsonb" NOT NULL, + "position" "jsonb" NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."dashboard_widgets" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."dashboards" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "organization_id" "uuid" NOT NULL, + "name" character varying NOT NULL, + "description" "text", + "layout" "jsonb" DEFAULT '[]'::"jsonb", + "is_default" boolean DEFAULT false, + "created_by" "uuid", + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."dashboards" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."generated_reports" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "template_id" "uuid" NOT NULL, + "website_id" "uuid", + "generated_by" "uuid", + "report_data" "jsonb" NOT NULL, + "format" character varying DEFAULT 'pdf'::character varying, + "file_url" character varying, + "created_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."generated_reports" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."metric_definitions" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "key" character varying NOT NULL, + "name" character varying NOT NULL, + "description" "text" NOT NULL, + "category" "public"."metric_category" NOT NULL, + "unit" character varying, + "is_core_metric" boolean DEFAULT false, + "default_threshold" numeric, + "warning_threshold" numeric, + "critical_threshold" numeric, + "direction" character varying DEFAULT 'higher_is_better'::character varying NOT NULL, + "weight" numeric DEFAULT 1.0, + "documentation_url" "text", + "created_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."metric_definitions" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."metric_values" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "scan_id" "uuid" NOT NULL, + "metric_id" "uuid" NOT NULL, + "value" numeric NOT NULL, + "raw_value" character varying, + "unit" character varying, + "is_passing" boolean, + "created_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."metric_values" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."notification_deliveries" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "alert_id" "uuid" NOT NULL, + "channel" "public"."notification_channel" NOT NULL, + "recipient" character varying NOT NULL, + "content" "text" NOT NULL, + "status" character varying DEFAULT 'pending'::character varying, + "sent_at" timestamp with time zone, + "error_message" "text", + "retry_count" integer DEFAULT 0, + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."notification_deliveries" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."organizations" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "name" character varying NOT NULL, + "subscription_tier" character varying DEFAULT 'free'::character varying, + "subscription_status" character varying DEFAULT 'active'::character varying, + "settings" "jsonb" DEFAULT '{"retention_days": 90, "alert_email_digest": "daily", "default_scan_depth": 3, "enable_competitor_analysis": false}'::"jsonb", + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."organizations" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."pages" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "website_id" "uuid" NOT NULL, + "url" character varying NOT NULL, + "path" character varying NOT NULL, + "title" character varying, + "description" "text", + "content_hash" character varying, + "content_type" character varying, + "status_code" integer, + "is_active" boolean DEFAULT true, + "priority" integer DEFAULT 1, + "depth" integer DEFAULT 0, + "parent_page_id" "uuid", + "discovery_method" character varying DEFAULT 'crawl'::character varying, + "last_seen_at" timestamp with time zone, + "metadata" "jsonb" DEFAULT '{"word_count": 0, "is_indexable": true, "has_canonical": false, "inbound_links": 0, "outbound_links": 0}'::"jsonb", + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."pages" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."performance_budgets" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "website_id" "uuid" NOT NULL, + "name" character varying NOT NULL, + "description" "text", + "metric_id" "uuid", + "budget_type" character varying NOT NULL, + "threshold" numeric NOT NULL, + "applies_to" "jsonb" DEFAULT '{"paths": ["/*"], "resource_types": ["all"]}'::"jsonb", + "is_active" boolean DEFAULT true, + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."performance_budgets" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."scans" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "website_id" "uuid" NOT NULL, + "page_id" "uuid" NOT NULL, + "triggered_by" "uuid", + "scan_type" character varying DEFAULT 'full'::character varying NOT NULL, + "status" "public"."scan_status" DEFAULT 'pending'::"public"."scan_status", + "priority" integer DEFAULT 1, + "categories" "public"."metric_category"[] DEFAULT ARRAY['performance'::"public"."metric_category", 'seo'::"public"."metric_category", 'accessibility'::"public"."metric_category", 'best_practices'::"public"."metric_category"], + "device_type" character varying DEFAULT 'desktop'::character varying, + "user_agent" character varying, + "lighthouse_version" character varying, + "chrome_version" character varying, + "environment" "jsonb" DEFAULT '{}'::"jsonb", + "started_at" timestamp with time zone DEFAULT "now"(), + "completed_at" timestamp with time zone, + "duration_ms" integer, + "error_message" "text", + "retry_count" integer DEFAULT 0, + "metadata" "jsonb" DEFAULT '{}'::"jsonb", + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"(), + "scheduled_at" timestamp with time zone +); + + +ALTER TABLE "public"."scans" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."websites" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "organization_id" "uuid" NOT NULL, + "base_url" character varying NOT NULL, + "name" character varying NOT NULL, + "is_active" boolean DEFAULT true, + "crawl_settings" "jsonb" DEFAULT '{"max_depth": 3, "max_pages": 100, "crawl_timing": "off_peak", "crawl_frequency": "daily", "exclude_patterns": ["/admin/*", "/api/*", "*.pdf", "*.jpg", "*.png"], "include_patterns": ["/*"], "respect_robots_txt": true}'::"jsonb", + "scan_schedule" "jsonb" DEFAULT '{"days": ["monday", "tuesday", "wednesday", "thursday", "friday"], "frequency": "hourly", "time_windows": ["0-6", "20-23"]}'::"jsonb", + "performance_budgets" "jsonb" DEFAULT '{"max_requests": 100, "page_weight_kb": 1000, "time_to_interactive_ms": 3000, "first_contentful_paint_ms": 1000}'::"jsonb", + "notifications" "jsonb" DEFAULT '{"channels": ["email"], "thresholds": {"seo": 90, "performance": 90, "accessibility": 90, "best_practices": 90}}'::"jsonb", + "last_crawl_at" timestamp with time zone, + "last_scan_at" timestamp with time zone, + "metadata" "jsonb" DEFAULT '{}'::"jsonb", + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."websites" OWNER TO "postgres"; + + +CREATE OR REPLACE VIEW "public"."performance_trends" WITH ("security_invoker"='on') AS + SELECT "w"."id" AS "website_id", + "w"."name" AS "website_name", + "p"."url" AS "page_url", + "m"."key" AS "metric_key", + "mv"."value" AS "metric_value", + "s"."created_at" AS "scan_date", + "avg"("mv"."value") OVER (PARTITION BY "w"."id", "p"."id", "m"."id" ORDER BY "s"."created_at" ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS "rolling_average" + FROM (((("public"."websites" "w" + JOIN "public"."pages" "p" ON (("p"."website_id" = "w"."id"))) + JOIN "public"."scans" "s" ON (("s"."page_id" = "p"."id"))) + JOIN "public"."metric_values" "mv" ON (("mv"."scan_id" = "s"."id"))) + JOIN "public"."metric_definitions" "m" ON (("m"."id" = "mv"."metric_id"))) + WHERE ("s"."created_at" >= ("now"() - '30 days'::interval)); + + +ALTER TABLE "public"."performance_trends" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."rate_limits" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "key_type" character varying NOT NULL, + "key_value" character varying NOT NULL, + "window_start" timestamp with time zone NOT NULL, + "request_count" integer DEFAULT 1, + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."rate_limits" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."report_templates" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "organization_id" "uuid" NOT NULL, + "name" character varying NOT NULL, + "description" "text", + "template_type" character varying NOT NULL, + "content" "jsonb" NOT NULL, + "schedule" "jsonb", + "is_active" boolean DEFAULT true, + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."report_templates" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."resource_analysis" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "scan_id" "uuid" NOT NULL, + "resource_type" "public"."resource_type" NOT NULL, + "url" character varying NOT NULL, + "size_bytes" integer NOT NULL, + "transfer_size_bytes" integer, + "duration_ms" integer, + "is_third_party" boolean DEFAULT false, + "is_cached" boolean, + "compression_ratio" numeric, + "mime_type" character varying, + "protocol" character varying, + "priority" character varying, + "status_code" integer, + "metadata" "jsonb" DEFAULT '{}'::"jsonb", + "created_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."resource_analysis" OWNER TO "postgres"; + + +CREATE OR REPLACE VIEW "public"."resource_usage_summary" WITH ("security_invoker"='on') AS + SELECT "w"."id" AS "website_id", + "w"."name" AS "website_name", + "ra"."resource_type", + "count"(*) AS "resource_count", + "avg"("ra"."size_bytes") AS "avg_size", + "sum"("ra"."size_bytes") AS "total_size", + "avg"("ra"."duration_ms") AS "avg_duration" + FROM (("public"."websites" "w" + JOIN "public"."scans" "s" ON (("s"."website_id" = "w"."id"))) + JOIN "public"."resource_analysis" "ra" ON (("ra"."scan_id" = "s"."id"))) + WHERE ("s"."created_at" >= ("now"() - '24:00:00'::interval)) + GROUP BY "w"."id", "w"."name", "ra"."resource_type"; + + +ALTER TABLE "public"."resource_usage_summary" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."scan_results" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "scan_id" "uuid" NOT NULL, + "category" "public"."metric_category" NOT NULL, + "score" numeric, + "raw_data" "jsonb", + "created_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."scan_results" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."url_patterns" ( + "id" "uuid" DEFAULT "extensions"."uuid_generate_v4"() NOT NULL, + "website_id" "uuid" NOT NULL, + "pattern" character varying NOT NULL, + "pattern_type" character varying NOT NULL, + "description" "text", + "is_regex" boolean DEFAULT false, + "is_active" boolean DEFAULT true, + "priority" integer DEFAULT 1, + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."url_patterns" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."users" ( + "id" "uuid" NOT NULL, + "email" character varying NOT NULL, + "name" character varying, + "organization_id" "uuid", + "role" character varying DEFAULT 'viewer'::character varying, + "is_active" boolean DEFAULT true, + "settings" "jsonb" DEFAULT '{"dashboard_layout": "default", "email_notifications": true, "notification_frequency": "instant"}'::"jsonb", + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"() +); + + +ALTER TABLE "public"."users" OWNER TO "postgres"; + + +ALTER TABLE ONLY "public"."alert_configurations" + ADD CONSTRAINT "alert_configurations_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."alert_history" + ADD CONSTRAINT "alert_history_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."alerts" + ADD CONSTRAINT "alerts_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."api_keys" + ADD CONSTRAINT "api_keys_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."audit_logs" + ADD CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."budget_violations" + ADD CONSTRAINT "budget_violations_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."competitor_websites" + ADD CONSTRAINT "competitor_websites_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."competitor_websites" + ADD CONSTRAINT "competitor_websites_website_id_competitor_url_key" UNIQUE ("website_id", "competitor_url"); + + + +ALTER TABLE ONLY "public"."crawl_queue" + ADD CONSTRAINT "crawl_queue_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."crawl_sessions" + ADD CONSTRAINT "crawl_sessions_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."dashboard_widgets" + ADD CONSTRAINT "dashboard_widgets_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."dashboards" + ADD CONSTRAINT "dashboards_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."generated_reports" + ADD CONSTRAINT "generated_reports_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."metric_definitions" + ADD CONSTRAINT "metric_definitions_key_key" UNIQUE ("key"); + + + +ALTER TABLE ONLY "public"."metric_definitions" + ADD CONSTRAINT "metric_definitions_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."metric_values" + ADD CONSTRAINT "metric_values_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."notification_deliveries" + ADD CONSTRAINT "notification_deliveries_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."organizations" + ADD CONSTRAINT "organizations_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."pages" + ADD CONSTRAINT "pages_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."pages" + ADD CONSTRAINT "pages_website_id_url_key" UNIQUE ("website_id", "url"); + + + +ALTER TABLE ONLY "public"."performance_budgets" + ADD CONSTRAINT "performance_budgets_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."rate_limits" + ADD CONSTRAINT "rate_limits_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."report_templates" + ADD CONSTRAINT "report_templates_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."resource_analysis" + ADD CONSTRAINT "resource_analysis_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."scan_results" + ADD CONSTRAINT "scan_results_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."scans" + ADD CONSTRAINT "scans_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."url_patterns" + ADD CONSTRAINT "url_patterns_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."users" + ADD CONSTRAINT "users_email_key" UNIQUE ("email"); + + + +ALTER TABLE ONLY "public"."users" + ADD CONSTRAINT "users_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."websites" + ADD CONSTRAINT "websites_organization_id_base_url_key" UNIQUE ("organization_id", "base_url"); + + + +ALTER TABLE ONLY "public"."websites" + ADD CONSTRAINT "websites_pkey" PRIMARY KEY ("id"); + + + +CREATE INDEX "idx_alert_configurations_metric_id" ON "public"."alert_configurations" USING "btree" ("metric_id"); + + + +CREATE INDEX "idx_alert_configurations_website_id" ON "public"."alert_configurations" USING "btree" ("website_id"); + + + +CREATE INDEX "idx_alert_history_alert_id" ON "public"."alert_history" USING "btree" ("alert_id"); + + + +CREATE INDEX "idx_alerts_config_id" ON "public"."alerts" USING "btree" ("config_id"); + + + +CREATE INDEX "idx_alerts_metric_id" ON "public"."alerts" USING "btree" ("metric_id"); + + + +CREATE INDEX "idx_alerts_page_id" ON "public"."alerts" USING "btree" ("page_id"); + + + +CREATE INDEX "idx_alerts_website_id" ON "public"."alerts" USING "btree" ("website_id"); + + + +CREATE INDEX "idx_budget_violations_budget_id" ON "public"."budget_violations" USING "btree" ("budget_id"); + + + +CREATE INDEX "idx_budget_violations_scan_id" ON "public"."budget_violations" USING "btree" ("scan_id"); + + + +CREATE INDEX "idx_crawl_queue_website_id" ON "public"."crawl_queue" USING "btree" ("website_id"); + + + +CREATE INDEX "idx_crawl_sessions_website_id" ON "public"."crawl_sessions" USING "btree" ("website_id"); + + + +CREATE INDEX "idx_dashboard_widgets_dashboard_id" ON "public"."dashboard_widgets" USING "btree" ("dashboard_id"); + + + +CREATE INDEX "idx_generated_reports_template_id" ON "public"."generated_reports" USING "btree" ("template_id"); + + + +CREATE INDEX "idx_generated_reports_website_id" ON "public"."generated_reports" USING "btree" ("website_id"); + + + +CREATE INDEX "idx_metric_values_metric_id" ON "public"."metric_values" USING "btree" ("metric_id"); + + + +CREATE INDEX "idx_metric_values_scan_id" ON "public"."metric_values" USING "btree" ("scan_id"); + + + +CREATE INDEX "idx_notification_deliveries_alert_id" ON "public"."notification_deliveries" USING "btree" ("alert_id"); + + + +CREATE INDEX "idx_pages_parent_page_id" ON "public"."pages" USING "btree" ("parent_page_id"); + + + +CREATE INDEX "idx_performance_budgets_metric_id" ON "public"."performance_budgets" USING "btree" ("metric_id"); + + + +CREATE INDEX "idx_performance_budgets_website_id" ON "public"."performance_budgets" USING "btree" ("website_id"); + + + +CREATE INDEX "idx_resource_analysis_scan_id" ON "public"."resource_analysis" USING "btree" ("scan_id"); + + + +CREATE INDEX "idx_scan_results_scan_id" ON "public"."scan_results" USING "btree" ("scan_id"); + + + +CREATE INDEX "idx_scans_page_id" ON "public"."scans" USING "btree" ("page_id"); + + + +CREATE INDEX "idx_scans_website_id" ON "public"."scans" USING "btree" ("website_id"); + + + +CREATE INDEX "idx_url_patterns_website_id" ON "public"."url_patterns" USING "btree" ("website_id"); + + + +CREATE INDEX "idx_users_organization_id" ON "public"."users" USING "btree" ("organization_id"); + + + +CREATE OR REPLACE TRIGGER "alert_notification_trigger" AFTER INSERT ON "public"."alerts" FOR EACH ROW EXECUTE FUNCTION "public"."process_alert_notifications"(); + + + +CREATE OR REPLACE TRIGGER "scan_status_update" AFTER UPDATE OF "status" ON "public"."scans" FOR EACH ROW WHEN (("old"."status" IS DISTINCT FROM "new"."status")) EXECUTE FUNCTION "public"."update_scan_status"(); + + + +ALTER TABLE ONLY "public"."alert_configurations" + ADD CONSTRAINT "alert_configurations_metric_id_fkey" FOREIGN KEY ("metric_id") REFERENCES "public"."metric_definitions"("id"); + + + +ALTER TABLE ONLY "public"."alert_configurations" + ADD CONSTRAINT "alert_configurations_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "public"."websites"("id"); + + + +ALTER TABLE ONLY "public"."alert_history" + ADD CONSTRAINT "alert_history_alert_id_fkey" FOREIGN KEY ("alert_id") REFERENCES "public"."alerts"("id"); + + + +ALTER TABLE ONLY "public"."alerts" + ADD CONSTRAINT "alerts_config_id_fkey" FOREIGN KEY ("config_id") REFERENCES "public"."alert_configurations"("id"); + + + +ALTER TABLE ONLY "public"."alerts" + ADD CONSTRAINT "alerts_metric_id_fkey" FOREIGN KEY ("metric_id") REFERENCES "public"."metric_definitions"("id"); + + + +ALTER TABLE ONLY "public"."alerts" + ADD CONSTRAINT "alerts_page_id_fkey" FOREIGN KEY ("page_id") REFERENCES "public"."pages"("id"); + + + +ALTER TABLE ONLY "public"."alerts" + ADD CONSTRAINT "alerts_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "public"."websites"("id"); + + + +ALTER TABLE ONLY "public"."budget_violations" + ADD CONSTRAINT "budget_violations_budget_id_fkey" FOREIGN KEY ("budget_id") REFERENCES "public"."performance_budgets"("id"); + + + +ALTER TABLE ONLY "public"."budget_violations" + ADD CONSTRAINT "budget_violations_scan_id_fkey" FOREIGN KEY ("scan_id") REFERENCES "public"."scans"("id"); + + + +ALTER TABLE ONLY "public"."competitor_websites" + ADD CONSTRAINT "competitor_websites_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "public"."websites"("id"); + + + +ALTER TABLE ONLY "public"."crawl_queue" + ADD CONSTRAINT "crawl_queue_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "public"."websites"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."crawl_sessions" + ADD CONSTRAINT "crawl_sessions_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "public"."websites"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."dashboard_widgets" + ADD CONSTRAINT "dashboard_widgets_dashboard_id_fkey" FOREIGN KEY ("dashboard_id") REFERENCES "public"."dashboards"("id"); + + + +ALTER TABLE ONLY "public"."users" + ADD CONSTRAINT "fk_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."generated_reports" + ADD CONSTRAINT "generated_reports_template_id_fkey" FOREIGN KEY ("template_id") REFERENCES "public"."report_templates"("id"); + + + +ALTER TABLE ONLY "public"."generated_reports" + ADD CONSTRAINT "generated_reports_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "public"."websites"("id"); + + + +ALTER TABLE ONLY "public"."metric_values" + ADD CONSTRAINT "metric_values_metric_id_fkey" FOREIGN KEY ("metric_id") REFERENCES "public"."metric_definitions"("id"); + + + +ALTER TABLE ONLY "public"."metric_values" + ADD CONSTRAINT "metric_values_scan_id_fkey" FOREIGN KEY ("scan_id") REFERENCES "public"."scans"("id"); + + + +ALTER TABLE ONLY "public"."notification_deliveries" + ADD CONSTRAINT "notification_deliveries_alert_id_fkey" FOREIGN KEY ("alert_id") REFERENCES "public"."alerts"("id"); + + + +ALTER TABLE ONLY "public"."pages" + ADD CONSTRAINT "pages_parent_page_id_fkey" FOREIGN KEY ("parent_page_id") REFERENCES "public"."pages"("id"); + + + +ALTER TABLE ONLY "public"."pages" + ADD CONSTRAINT "pages_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "public"."websites"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."performance_budgets" + ADD CONSTRAINT "performance_budgets_metric_id_fkey" FOREIGN KEY ("metric_id") REFERENCES "public"."metric_definitions"("id"); + + + +ALTER TABLE ONLY "public"."performance_budgets" + ADD CONSTRAINT "performance_budgets_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "public"."websites"("id"); + + + +ALTER TABLE ONLY "public"."resource_analysis" + ADD CONSTRAINT "resource_analysis_scan_id_fkey" FOREIGN KEY ("scan_id") REFERENCES "public"."scans"("id"); + + + +ALTER TABLE ONLY "public"."scan_results" + ADD CONSTRAINT "scan_results_scan_id_fkey" FOREIGN KEY ("scan_id") REFERENCES "public"."scans"("id"); + + + +ALTER TABLE ONLY "public"."scans" + ADD CONSTRAINT "scans_page_id_fkey" FOREIGN KEY ("page_id") REFERENCES "public"."pages"("id"); + + + +ALTER TABLE ONLY "public"."scans" + ADD CONSTRAINT "scans_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "public"."websites"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."url_patterns" + ADD CONSTRAINT "url_patterns_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "public"."websites"("id"); + + + +ALTER TABLE ONLY "public"."users" + ADD CONSTRAINT "users_id_fkey" FOREIGN KEY ("id") REFERENCES "auth"."users"("id"); + + + +ALTER TABLE ONLY "public"."users" + ADD CONSTRAINT "users_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id"); + + + +CREATE POLICY "Allow organization insert for authenticated" ON "public"."organizations" FOR INSERT TO "authenticated" WITH CHECK (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."alert_configurations" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."alert_history" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."audit_logs" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."budget_violations" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."competitor_websites" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."crawl_queue" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."crawl_sessions" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."dashboard_widgets" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."generated_reports" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."metric_definitions" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."notification_deliveries" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."performance_budgets" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."rate_limits" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."report_templates" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."resource_analysis" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."scan_results" FOR SELECT USING (true); + + + +CREATE POLICY "Allow read for authenticated users" ON "public"."url_patterns" FOR SELECT USING (true); + + + +CREATE POLICY "Allow user insert for anon" ON "public"."users" FOR INSERT TO "anon" WITH CHECK (true); + + + +CREATE POLICY "Allow user insert for authenticated" ON "public"."users" FOR INSERT TO "authenticated" WITH CHECK (true); + + + +CREATE POLICY "Allow user insert for authenticator" ON "public"."users" FOR INSERT TO "authenticator" WITH CHECK (true); + + + +CREATE POLICY "Allow user insert for dashboard_user" ON "public"."users" FOR INSERT TO "dashboard_user" WITH CHECK (true); + + + +CREATE POLICY "Allow user update for anon" ON "public"."users" FOR UPDATE TO "anon" USING (("id" = ( SELECT "auth"."uid"() AS "uid"))); + + + +CREATE POLICY "Allow user update for authenticated" ON "public"."users" FOR UPDATE TO "authenticated" USING (("id" = ( SELECT "auth"."uid"() AS "uid"))); + + + +CREATE POLICY "Allow user update for authenticator" ON "public"."users" FOR UPDATE TO "authenticator" USING (("id" = ( SELECT "auth"."uid"() AS "uid"))); + + + +CREATE POLICY "Allow user update for dashboard_user" ON "public"."users" FOR UPDATE TO "dashboard_user" USING (("id" = ( SELECT "auth"."uid"() AS "uid"))); + + + +CREATE POLICY "Users can delete pages of their organization" ON "public"."pages" FOR DELETE USING (("website_id" IN ( SELECT "websites"."id" + FROM "public"."websites" + WHERE ("websites"."organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))))); + + + +CREATE POLICY "Users can delete scans of their organization" ON "public"."scans" FOR DELETE USING (("website_id" IN ( SELECT "w"."id" + FROM "public"."websites" "w" + WHERE ("w"."organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))))); + + + +CREATE POLICY "Users can delete websites of their organization" ON "public"."websites" FOR DELETE USING (("organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))); + + + +CREATE POLICY "Users can insert pages for their organization" ON "public"."pages" FOR INSERT WITH CHECK (("website_id" IN ( SELECT "websites"."id" + FROM "public"."websites" + WHERE ("websites"."organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))))); + + + +CREATE POLICY "Users can insert scans for their organization" ON "public"."scans" FOR INSERT WITH CHECK (("website_id" IN ( SELECT "w"."id" + FROM "public"."websites" "w" + WHERE ("w"."organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))))); + + + +CREATE POLICY "Users can insert websites for their organization" ON "public"."websites" FOR INSERT WITH CHECK (("organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))); + + + +CREATE POLICY "Users can select their own data" ON "public"."users" FOR SELECT USING (("auth"."uid"() = "id")); + + + +CREATE POLICY "Users can update pages of their organization" ON "public"."pages" FOR UPDATE USING (("website_id" IN ( SELECT "websites"."id" + FROM "public"."websites" + WHERE ("websites"."organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))))); + + + +CREATE POLICY "Users can update scans of their organization" ON "public"."scans" FOR UPDATE USING (("website_id" IN ( SELECT "w"."id" + FROM "public"."websites" "w" + WHERE ("w"."organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))))); + + + +CREATE POLICY "Users can update websites of their organization" ON "public"."websites" FOR UPDATE USING (("organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))); + + + +CREATE POLICY "Users can view alerts they are allowed to" ON "public"."alerts" FOR SELECT USING (("website_id" IN ( SELECT "w"."id" + FROM "public"."websites" "w" + WHERE ("w"."organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))))); + + + +CREATE POLICY "Users can view dashboards of their organization" ON "public"."dashboards" FOR SELECT USING (("organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))); + + + +CREATE POLICY "Users can view metric_values of their organization" ON "public"."metric_values" FOR SELECT USING (("scan_id" IN ( SELECT "s"."id" + FROM ("public"."scans" "s" + JOIN "public"."websites" "w" ON (("w"."id" = "s"."website_id"))) + WHERE ("w"."organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))))); + + + +CREATE POLICY "Users can view pages they are allowed to" ON "public"."pages" FOR SELECT USING (("website_id" IN ( SELECT "w"."id" + FROM "public"."websites" "w" + WHERE ("w"."organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))))); + + + +CREATE POLICY "Users can view scans they are allowed to" ON "public"."scans" FOR SELECT USING (("website_id" IN ( SELECT "w"."id" + FROM "public"."websites" "w" + WHERE ("w"."organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))))); + + + +CREATE POLICY "Users can view their org's API keys" ON "public"."api_keys" FOR SELECT USING (("organization_id" IN ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))); + + + +CREATE POLICY "Users can view their org's websites" ON "public"."websites" FOR SELECT USING (("organization_id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))); + + + +CREATE POLICY "Users can view their organization" ON "public"."organizations" FOR SELECT USING (("id" = ( SELECT "users"."organization_id" + FROM "public"."users" + WHERE ("users"."id" = ( SELECT "auth"."uid"() AS "uid"))))); + + + +CREATE POLICY "Users can view their own profile" ON "public"."users" FOR SELECT USING (("id" = ( SELECT "auth"."uid"() AS "uid"))); + + + +ALTER TABLE "public"."alert_configurations" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."alert_history" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."alerts" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."api_keys" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."audit_logs" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."budget_violations" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."competitor_websites" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."crawl_queue" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."crawl_sessions" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."dashboard_widgets" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."dashboards" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."generated_reports" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."metric_definitions" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."metric_values" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."notification_deliveries" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."organizations" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."pages" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."performance_budgets" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."rate_limits" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."report_templates" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."resource_analysis" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."scan_results" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."scans" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."url_patterns" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."users" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."websites" ENABLE ROW LEVEL SECURITY; + + + + +ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres"; + + + + + + +GRANT USAGE ON SCHEMA "public" TO "postgres"; +GRANT USAGE ON SCHEMA "public" TO "anon"; +GRANT USAGE ON SCHEMA "public" TO "authenticated"; +GRANT USAGE ON SCHEMA "public" TO "service_role"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +GRANT ALL ON FUNCTION "public"."apply_data_retention"() TO "anon"; +GRANT ALL ON FUNCTION "public"."apply_data_retention"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."apply_data_retention"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."calculate_competitor_comparison"("website_id" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."calculate_competitor_comparison"("website_id" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."calculate_competitor_comparison"("website_id" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."calculate_competitor_comparison"("website_id_param" "uuid", "competitor_id_param" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."calculate_competitor_comparison"("website_id_param" "uuid", "competitor_id_param" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."calculate_competitor_comparison"("website_id_param" "uuid", "competitor_id_param" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."calculate_health_score"("website_id_param" "uuid") TO "anon"; +GRANT ALL ON FUNCTION "public"."calculate_health_score"("website_id_param" "uuid") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."calculate_health_score"("website_id_param" "uuid") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."email_exists"("email_to_check" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."email_exists"("email_to_check" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."email_exists"("email_to_check" "text") TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."generate_performance_report"("website_id" "uuid", "days" integer) TO "anon"; +GRANT ALL ON FUNCTION "public"."generate_performance_report"("website_id" "uuid", "days" integer) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."generate_performance_report"("website_id" "uuid", "days" integer) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."generate_performance_report"("website_id_param" "uuid", "start_date" timestamp with time zone, "end_date" timestamp with time zone) TO "anon"; +GRANT ALL ON FUNCTION "public"."generate_performance_report"("website_id_param" "uuid", "start_date" timestamp with time zone, "end_date" timestamp with time zone) TO "authenticated"; +GRANT ALL ON FUNCTION "public"."generate_performance_report"("website_id_param" "uuid", "start_date" timestamp with time zone, "end_date" timestamp with time zone) TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "anon"; +GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."perform_maintenance"() TO "anon"; +GRANT ALL ON FUNCTION "public"."perform_maintenance"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."perform_maintenance"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."process_alert_notifications"() TO "anon"; +GRANT ALL ON FUNCTION "public"."process_alert_notifications"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."process_alert_notifications"() TO "service_role"; + + + +GRANT ALL ON FUNCTION "public"."update_scan_status"() TO "anon"; +GRANT ALL ON FUNCTION "public"."update_scan_status"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."update_scan_status"() TO "service_role"; + + + + + + + + + + + + + + + + + + + + + + + + + + + +GRANT ALL ON TABLE "public"."alert_configurations" TO "anon"; +GRANT ALL ON TABLE "public"."alert_configurations" TO "authenticated"; +GRANT ALL ON TABLE "public"."alert_configurations" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."alert_history" TO "anon"; +GRANT ALL ON TABLE "public"."alert_history" TO "authenticated"; +GRANT ALL ON TABLE "public"."alert_history" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."alerts" TO "anon"; +GRANT ALL ON TABLE "public"."alerts" TO "authenticated"; +GRANT ALL ON TABLE "public"."alerts" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."api_keys" TO "anon"; +GRANT ALL ON TABLE "public"."api_keys" TO "authenticated"; +GRANT ALL ON TABLE "public"."api_keys" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."audit_logs" TO "anon"; +GRANT ALL ON TABLE "public"."audit_logs" TO "authenticated"; +GRANT ALL ON TABLE "public"."audit_logs" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."budget_violations" TO "anon"; +GRANT ALL ON TABLE "public"."budget_violations" TO "authenticated"; +GRANT ALL ON TABLE "public"."budget_violations" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."competitor_websites" TO "anon"; +GRANT ALL ON TABLE "public"."competitor_websites" TO "authenticated"; +GRANT ALL ON TABLE "public"."competitor_websites" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."crawl_queue" TO "anon"; +GRANT ALL ON TABLE "public"."crawl_queue" TO "authenticated"; +GRANT ALL ON TABLE "public"."crawl_queue" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."crawl_sessions" TO "anon"; +GRANT ALL ON TABLE "public"."crawl_sessions" TO "authenticated"; +GRANT ALL ON TABLE "public"."crawl_sessions" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."dashboard_widgets" TO "anon"; +GRANT ALL ON TABLE "public"."dashboard_widgets" TO "authenticated"; +GRANT ALL ON TABLE "public"."dashboard_widgets" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."dashboards" TO "anon"; +GRANT ALL ON TABLE "public"."dashboards" TO "authenticated"; +GRANT ALL ON TABLE "public"."dashboards" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."generated_reports" TO "anon"; +GRANT ALL ON TABLE "public"."generated_reports" TO "authenticated"; +GRANT ALL ON TABLE "public"."generated_reports" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."metric_definitions" TO "anon"; +GRANT ALL ON TABLE "public"."metric_definitions" TO "authenticated"; +GRANT ALL ON TABLE "public"."metric_definitions" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."metric_values" TO "anon"; +GRANT ALL ON TABLE "public"."metric_values" TO "authenticated"; +GRANT ALL ON TABLE "public"."metric_values" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."notification_deliveries" TO "anon"; +GRANT ALL ON TABLE "public"."notification_deliveries" TO "authenticated"; +GRANT ALL ON TABLE "public"."notification_deliveries" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."organizations" TO "anon"; +GRANT ALL ON TABLE "public"."organizations" TO "authenticated"; +GRANT ALL ON TABLE "public"."organizations" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."pages" TO "anon"; +GRANT ALL ON TABLE "public"."pages" TO "authenticated"; +GRANT ALL ON TABLE "public"."pages" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."performance_budgets" TO "anon"; +GRANT ALL ON TABLE "public"."performance_budgets" TO "authenticated"; +GRANT ALL ON TABLE "public"."performance_budgets" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."scans" TO "anon"; +GRANT ALL ON TABLE "public"."scans" TO "authenticated"; +GRANT ALL ON TABLE "public"."scans" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."websites" TO "anon"; +GRANT ALL ON TABLE "public"."websites" TO "authenticated"; +GRANT ALL ON TABLE "public"."websites" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."performance_trends" TO "anon"; +GRANT ALL ON TABLE "public"."performance_trends" TO "authenticated"; +GRANT ALL ON TABLE "public"."performance_trends" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."rate_limits" TO "anon"; +GRANT ALL ON TABLE "public"."rate_limits" TO "authenticated"; +GRANT ALL ON TABLE "public"."rate_limits" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."report_templates" TO "anon"; +GRANT ALL ON TABLE "public"."report_templates" TO "authenticated"; +GRANT ALL ON TABLE "public"."report_templates" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."resource_analysis" TO "anon"; +GRANT ALL ON TABLE "public"."resource_analysis" TO "authenticated"; +GRANT ALL ON TABLE "public"."resource_analysis" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."resource_usage_summary" TO "anon"; +GRANT ALL ON TABLE "public"."resource_usage_summary" TO "authenticated"; +GRANT ALL ON TABLE "public"."resource_usage_summary" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."scan_results" TO "anon"; +GRANT ALL ON TABLE "public"."scan_results" TO "authenticated"; +GRANT ALL ON TABLE "public"."scan_results" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."url_patterns" TO "anon"; +GRANT ALL ON TABLE "public"."url_patterns" TO "authenticated"; +GRANT ALL ON TABLE "public"."url_patterns" TO "service_role"; + + + +GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,UPDATE ON TABLE "public"."users" TO "anon"; +GRANT ALL ON TABLE "public"."users" TO "authenticated"; +GRANT ALL ON TABLE "public"."users" TO "service_role"; + + + + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role"; + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role"; + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +RESET ALL; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102531_remote_schema.sql b/website-monitoring-frontend/supabase/migrations/20250813102531_remote_schema.sql new file mode 100644 index 0000000..027628b --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102531_remote_schema.sql @@ -0,0 +1,424 @@ +revoke delete on table "auth"."audit_log_entries" from "postgres"; + +revoke insert on table "auth"."audit_log_entries" from "postgres"; + +revoke references on table "auth"."audit_log_entries" from "postgres"; + +revoke select on table "auth"."audit_log_entries" from "postgres"; + +revoke trigger on table "auth"."audit_log_entries" from "postgres"; + +revoke truncate on table "auth"."audit_log_entries" from "postgres"; + +revoke update on table "auth"."audit_log_entries" from "postgres"; + +revoke delete on table "auth"."flow_state" from "postgres"; + +revoke insert on table "auth"."flow_state" from "postgres"; + +revoke references on table "auth"."flow_state" from "postgres"; + +revoke select on table "auth"."flow_state" from "postgres"; + +revoke trigger on table "auth"."flow_state" from "postgres"; + +revoke truncate on table "auth"."flow_state" from "postgres"; + +revoke update on table "auth"."flow_state" from "postgres"; + +revoke delete on table "auth"."identities" from "postgres"; + +revoke insert on table "auth"."identities" from "postgres"; + +revoke references on table "auth"."identities" from "postgres"; + +revoke select on table "auth"."identities" from "postgres"; + +revoke trigger on table "auth"."identities" from "postgres"; + +revoke truncate on table "auth"."identities" from "postgres"; + +revoke update on table "auth"."identities" from "postgres"; + +revoke delete on table "auth"."instances" from "postgres"; + +revoke insert on table "auth"."instances" from "postgres"; + +revoke references on table "auth"."instances" from "postgres"; + +revoke select on table "auth"."instances" from "postgres"; + +revoke trigger on table "auth"."instances" from "postgres"; + +revoke truncate on table "auth"."instances" from "postgres"; + +revoke update on table "auth"."instances" from "postgres"; + +revoke delete on table "auth"."mfa_amr_claims" from "postgres"; + +revoke insert on table "auth"."mfa_amr_claims" from "postgres"; + +revoke references on table "auth"."mfa_amr_claims" from "postgres"; + +revoke select on table "auth"."mfa_amr_claims" from "postgres"; + +revoke trigger on table "auth"."mfa_amr_claims" from "postgres"; + +revoke truncate on table "auth"."mfa_amr_claims" from "postgres"; + +revoke update on table "auth"."mfa_amr_claims" from "postgres"; + +revoke delete on table "auth"."mfa_challenges" from "postgres"; + +revoke insert on table "auth"."mfa_challenges" from "postgres"; + +revoke references on table "auth"."mfa_challenges" from "postgres"; + +revoke select on table "auth"."mfa_challenges" from "postgres"; + +revoke trigger on table "auth"."mfa_challenges" from "postgres"; + +revoke truncate on table "auth"."mfa_challenges" from "postgres"; + +revoke update on table "auth"."mfa_challenges" from "postgres"; + +revoke delete on table "auth"."mfa_factors" from "postgres"; + +revoke insert on table "auth"."mfa_factors" from "postgres"; + +revoke references on table "auth"."mfa_factors" from "postgres"; + +revoke select on table "auth"."mfa_factors" from "postgres"; + +revoke trigger on table "auth"."mfa_factors" from "postgres"; + +revoke truncate on table "auth"."mfa_factors" from "postgres"; + +revoke update on table "auth"."mfa_factors" from "postgres"; + +revoke delete on table "auth"."one_time_tokens" from "postgres"; + +revoke insert on table "auth"."one_time_tokens" from "postgres"; + +revoke references on table "auth"."one_time_tokens" from "postgres"; + +revoke select on table "auth"."one_time_tokens" from "postgres"; + +revoke trigger on table "auth"."one_time_tokens" from "postgres"; + +revoke truncate on table "auth"."one_time_tokens" from "postgres"; + +revoke update on table "auth"."one_time_tokens" from "postgres"; + +revoke delete on table "auth"."refresh_tokens" from "postgres"; + +revoke insert on table "auth"."refresh_tokens" from "postgres"; + +revoke references on table "auth"."refresh_tokens" from "postgres"; + +revoke select on table "auth"."refresh_tokens" from "postgres"; + +revoke trigger on table "auth"."refresh_tokens" from "postgres"; + +revoke truncate on table "auth"."refresh_tokens" from "postgres"; + +revoke update on table "auth"."refresh_tokens" from "postgres"; + +revoke delete on table "auth"."saml_providers" from "postgres"; + +revoke insert on table "auth"."saml_providers" from "postgres"; + +revoke references on table "auth"."saml_providers" from "postgres"; + +revoke select on table "auth"."saml_providers" from "postgres"; + +revoke trigger on table "auth"."saml_providers" from "postgres"; + +revoke truncate on table "auth"."saml_providers" from "postgres"; + +revoke update on table "auth"."saml_providers" from "postgres"; + +revoke delete on table "auth"."saml_relay_states" from "postgres"; + +revoke insert on table "auth"."saml_relay_states" from "postgres"; + +revoke references on table "auth"."saml_relay_states" from "postgres"; + +revoke select on table "auth"."saml_relay_states" from "postgres"; + +revoke trigger on table "auth"."saml_relay_states" from "postgres"; + +revoke truncate on table "auth"."saml_relay_states" from "postgres"; + +revoke update on table "auth"."saml_relay_states" from "postgres"; + +revoke select on table "auth"."schema_migrations" from "postgres"; + +revoke delete on table "auth"."sessions" from "postgres"; + +revoke insert on table "auth"."sessions" from "postgres"; + +revoke references on table "auth"."sessions" from "postgres"; + +revoke select on table "auth"."sessions" from "postgres"; + +revoke trigger on table "auth"."sessions" from "postgres"; + +revoke truncate on table "auth"."sessions" from "postgres"; + +revoke update on table "auth"."sessions" from "postgres"; + +revoke delete on table "auth"."sso_domains" from "postgres"; + +revoke insert on table "auth"."sso_domains" from "postgres"; + +revoke references on table "auth"."sso_domains" from "postgres"; + +revoke select on table "auth"."sso_domains" from "postgres"; + +revoke trigger on table "auth"."sso_domains" from "postgres"; + +revoke truncate on table "auth"."sso_domains" from "postgres"; + +revoke update on table "auth"."sso_domains" from "postgres"; + +revoke delete on table "auth"."sso_providers" from "postgres"; + +revoke insert on table "auth"."sso_providers" from "postgres"; + +revoke references on table "auth"."sso_providers" from "postgres"; + +revoke select on table "auth"."sso_providers" from "postgres"; + +revoke trigger on table "auth"."sso_providers" from "postgres"; + +revoke truncate on table "auth"."sso_providers" from "postgres"; + +revoke update on table "auth"."sso_providers" from "postgres"; + +revoke delete on table "auth"."users" from "postgres"; + +revoke insert on table "auth"."users" from "postgres"; + +revoke references on table "auth"."users" from "postgres"; + +revoke select on table "auth"."users" from "postgres"; + +revoke trigger on table "auth"."users" from "postgres"; + +revoke truncate on table "auth"."users" from "postgres"; + +revoke update on table "auth"."users" from "postgres"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.handle_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public' +AS $function$ +DECLARE + organization_id UUID; +BEGIN + -- First create organization if it doesn't exist + INSERT INTO public.organizations ( + name, + subscription_tier, + subscription_status + ) VALUES ( + COALESCE(NEW.raw_user_meta_data->>'organization_name', NEW.raw_user_meta_data->>'name' || '''s Organization'), + 'free', + 'active' + ) + RETURNING id INTO organization_id; + + -- Then create the user profile + INSERT INTO public.users ( + id, + email, + name, + organization_id, + role, + is_active, + settings, + created_at, + updated_at + ) VALUES ( + NEW.id, + NEW.email, + COALESCE(NEW.raw_user_meta_data->>'name', split_part(NEW.email, '@', 1)), + organization_id, + COALESCE(NEW.raw_user_meta_data->>'role', 'viewer')::user_role, + true, + jsonb_build_object( + 'email_notifications', true, + 'notification_frequency', 'instant', + 'dashboard_layout', 'default' + ), + NOW(), + NOW() + ); + + RETURN NEW; +EXCEPTION + WHEN others THEN + -- Log the error (will appear in Postgres logs) + RAISE LOG 'Error in handle_new_user: %', SQLERRM; + RETURN NEW; +END; +$function$ +; + +DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; +CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); + +revoke delete on table "storage"."buckets" from "anon"; + +revoke insert on table "storage"."buckets" from "anon"; + +revoke references on table "storage"."buckets" from "anon"; + +revoke select on table "storage"."buckets" from "anon"; + +revoke trigger on table "storage"."buckets" from "anon"; + +revoke truncate on table "storage"."buckets" from "anon"; + +revoke update on table "storage"."buckets" from "anon"; + +revoke delete on table "storage"."buckets" from "authenticated"; + +revoke insert on table "storage"."buckets" from "authenticated"; + +revoke references on table "storage"."buckets" from "authenticated"; + +revoke select on table "storage"."buckets" from "authenticated"; + +revoke trigger on table "storage"."buckets" from "authenticated"; + +revoke truncate on table "storage"."buckets" from "authenticated"; + +revoke update on table "storage"."buckets" from "authenticated"; + +revoke delete on table "storage"."buckets" from "postgres"; + +revoke insert on table "storage"."buckets" from "postgres"; + +revoke references on table "storage"."buckets" from "postgres"; + +revoke select on table "storage"."buckets" from "postgres"; + +revoke trigger on table "storage"."buckets" from "postgres"; + +revoke truncate on table "storage"."buckets" from "postgres"; + +revoke update on table "storage"."buckets" from "postgres"; + +revoke delete on table "storage"."buckets" from "service_role"; + +revoke insert on table "storage"."buckets" from "service_role"; + +revoke references on table "storage"."buckets" from "service_role"; + +revoke select on table "storage"."buckets" from "service_role"; + +revoke trigger on table "storage"."buckets" from "service_role"; + +revoke truncate on table "storage"."buckets" from "service_role"; + +revoke update on table "storage"."buckets" from "service_role"; + +revoke delete on table "storage"."objects" from "anon"; + +revoke insert on table "storage"."objects" from "anon"; + +revoke references on table "storage"."objects" from "anon"; + +revoke select on table "storage"."objects" from "anon"; + +revoke trigger on table "storage"."objects" from "anon"; + +revoke truncate on table "storage"."objects" from "anon"; + +revoke update on table "storage"."objects" from "anon"; + +revoke delete on table "storage"."objects" from "authenticated"; + +revoke insert on table "storage"."objects" from "authenticated"; + +revoke references on table "storage"."objects" from "authenticated"; + +revoke select on table "storage"."objects" from "authenticated"; + +revoke trigger on table "storage"."objects" from "authenticated"; + +revoke truncate on table "storage"."objects" from "authenticated"; + +revoke update on table "storage"."objects" from "authenticated"; + +revoke delete on table "storage"."objects" from "postgres"; + +revoke insert on table "storage"."objects" from "postgres"; + +revoke references on table "storage"."objects" from "postgres"; + +revoke select on table "storage"."objects" from "postgres"; + +revoke trigger on table "storage"."objects" from "postgres"; + +revoke truncate on table "storage"."objects" from "postgres"; + +revoke update on table "storage"."objects" from "postgres"; + +revoke delete on table "storage"."objects" from "service_role"; + +revoke insert on table "storage"."objects" from "service_role"; + +revoke references on table "storage"."objects" from "service_role"; + +revoke select on table "storage"."objects" from "service_role"; + +revoke trigger on table "storage"."objects" from "service_role"; + +revoke truncate on table "storage"."objects" from "service_role"; + +revoke update on table "storage"."objects" from "service_role"; + +revoke select on table "storage"."s3_multipart_uploads" from "anon"; + +revoke select on table "storage"."s3_multipart_uploads" from "authenticated"; + +revoke delete on table "storage"."s3_multipart_uploads" from "service_role"; + +revoke insert on table "storage"."s3_multipart_uploads" from "service_role"; + +revoke references on table "storage"."s3_multipart_uploads" from "service_role"; + +revoke select on table "storage"."s3_multipart_uploads" from "service_role"; + +revoke trigger on table "storage"."s3_multipart_uploads" from "service_role"; + +revoke truncate on table "storage"."s3_multipart_uploads" from "service_role"; + +revoke update on table "storage"."s3_multipart_uploads" from "service_role"; + +revoke select on table "storage"."s3_multipart_uploads_parts" from "anon"; + +revoke select on table "storage"."s3_multipart_uploads_parts" from "authenticated"; + +revoke delete on table "storage"."s3_multipart_uploads_parts" from "service_role"; + +revoke insert on table "storage"."s3_multipart_uploads_parts" from "service_role"; + +revoke references on table "storage"."s3_multipart_uploads_parts" from "service_role"; + +revoke select on table "storage"."s3_multipart_uploads_parts" from "service_role"; + +revoke trigger on table "storage"."s3_multipart_uploads_parts" from "service_role"; + +revoke truncate on table "storage"."s3_multipart_uploads_parts" from "service_role"; + +revoke update on table "storage"."s3_multipart_uploads_parts" from "service_role"; + + diff --git a/website-monitoring-frontend/supabase/migrations/20250813102532_add_missing_columns.sql b/website-monitoring-frontend/supabase/migrations/20250813102532_add_missing_columns.sql new file mode 100644 index 0000000..bf1888e --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102532_add_missing_columns.sql @@ -0,0 +1,86 @@ +-- Add missing enum types +DO $$ BEGIN + CREATE TYPE scan_status AS ENUM ('pending', 'queued', 'running', 'completed', 'failed', 'cancelled'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE severity_level AS ENUM ('low', 'medium', 'high', 'critical'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE comparison_operator AS ENUM ('equals', 'not_equals', 'greater_than', 'less_than', 'contains', 'not_contains'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Add missing columns to scans table +ALTER TABLE scans ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMP WITH TIME ZONE; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS trigger_type TEXT DEFAULT 'manual'; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS website_id UUID REFERENCES websites(id); +ALTER TABLE scans ADD COLUMN IF NOT EXISTS triggered_by UUID REFERENCES auth.users(id); +ALTER TABLE scans ADD COLUMN IF NOT EXISTS scan_type TEXT DEFAULT 'lighthouse'; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS priority INTEGER DEFAULT 5; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS categories TEXT[] DEFAULT ARRAY['performance', 'seo', 'accessibility', 'best_practices']; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS device_type TEXT DEFAULT 'desktop'; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS user_agent TEXT; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS lighthouse_version TEXT; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS chrome_version TEXT; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS environment TEXT DEFAULT 'production'; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS started_at TIMESTAMP WITH TIME ZONE; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS completed_at TIMESTAMP WITH TIME ZONE; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS duration_ms INTEGER; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS error_message TEXT; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS retry_count INTEGER DEFAULT 0; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'; +ALTER TABLE scans ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(); + +-- Add missing columns to pages table +ALTER TABLE pages ADD COLUMN IF NOT EXISTS status scan_status DEFAULT 'pending'; +ALTER TABLE pages ADD COLUMN IF NOT EXISTS last_scanned_at TIMESTAMP WITH TIME ZONE; +ALTER TABLE pages ADD COLUMN IF NOT EXISTS scan_count INTEGER DEFAULT 0; +ALTER TABLE pages ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'; + +-- Add missing columns to websites table +ALTER TABLE websites ADD COLUMN IF NOT EXISTS crawl_settings JSONB DEFAULT '{}'; +ALTER TABLE websites ADD COLUMN IF NOT EXISTS alert_settings JSONB DEFAULT '{}'; +ALTER TABLE websites ADD COLUMN IF NOT EXISTS last_crawled_at TIMESTAMP WITH TIME ZONE; +ALTER TABLE websites ADD COLUMN IF NOT EXISTS crawl_session_id UUID; +ALTER TABLE websites ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'; + +-- Add missing columns to users table +ALTER TABLE users ADD COLUMN IF NOT EXISTS organization_id UUID REFERENCES organizations(id); +ALTER TABLE users ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'user'; +ALTER TABLE users ADD COLUMN IF NOT EXISTS preferences JSONB DEFAULT '{}'; + +-- Add missing columns to organizations table +ALTER TABLE organizations ADD COLUMN IF NOT EXISTS settings JSONB DEFAULT '{}'; +ALTER TABLE organizations ADD COLUMN IF NOT EXISTS subscription_plan TEXT DEFAULT 'free'; +ALTER TABLE organizations ADD COLUMN IF NOT EXISTS subscription_status TEXT DEFAULT 'active'; + +-- Create indexes for better performance +CREATE INDEX IF NOT EXISTS idx_scans_website_id ON scans(website_id); +CREATE INDEX IF NOT EXISTS idx_scans_status ON scans(status); +CREATE INDEX IF NOT EXISTS idx_scans_created_at ON scans(created_at); +CREATE INDEX IF NOT EXISTS idx_pages_website_id ON pages(website_id); +CREATE INDEX IF NOT EXISTS idx_pages_status ON pages(status); +CREATE INDEX IF NOT EXISTS idx_users_organization_id ON users(organization_id); + +-- Add triggers for updated_at +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_scans_updated_at BEFORE UPDATE ON scans FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_websites_updated_at BEFORE UPDATE ON websites FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_pages_updated_at BEFORE UPDATE ON pages FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- Refresh schema cache +NOTIFY pgrst, 'reload schema'; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102533_fix_organization_rls.sql b/website-monitoring-frontend/supabase/migrations/20250813102533_fix_organization_rls.sql new file mode 100644 index 0000000..cdfa2a4 --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102533_fix_organization_rls.sql @@ -0,0 +1,13 @@ +-- Fix RLS policies for organization creation during registration +-- Allow anonymous users to create organizations during signup + +-- Add policy for anonymous organization creation +CREATE POLICY "Allow organization insert for anon during registration" ON "public"."organizations" +FOR INSERT TO "anon" WITH CHECK (true); + +-- Add policy for anonymous organization selection (needed to get the created org) +CREATE POLICY "Allow organization select for anon during registration" ON "public"."organizations" +FOR SELECT TO "anon" USING (true); + +-- Refresh schema cache +NOTIFY pgrst, 'reload schema'; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102534_fix_user_rls.sql b/website-monitoring-frontend/supabase/migrations/20250813102534_fix_user_rls.sql new file mode 100644 index 0000000..05e78bd --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102534_fix_user_rls.sql @@ -0,0 +1,14 @@ +-- Fix RLS policies for user profile creation during registration +-- The issue is that during registration, auth.uid() might not be available yet + +-- Add a more permissive policy for user profile creation during registration +-- This allows inserting user profiles with the user's own ID +CREATE POLICY "Allow user profile creation during registration" ON "public"."users" +FOR INSERT TO "anon" WITH CHECK (true); + +-- Also allow authenticated users to create their own profile +CREATE POLICY "Allow authenticated user profile creation" ON "public"."users" +FOR INSERT TO "authenticated" WITH CHECK (auth.uid() = id); + +-- Refresh schema cache +NOTIFY pgrst, 'reload schema'; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102535_fix_user_rls_final.sql b/website-monitoring-frontend/supabase/migrations/20250813102535_fix_user_rls_final.sql new file mode 100644 index 0000000..6983128 --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102535_fix_user_rls_final.sql @@ -0,0 +1,24 @@ +-- Final fix for user RLS policies +-- Drop the conflicting policies and create the correct ones + +-- Drop existing conflicting policies +DROP POLICY IF EXISTS "Allow user insert for anon" ON "public"."users"; +DROP POLICY IF EXISTS "Allow user insert for authenticated" ON "public"."users"; +DROP POLICY IF EXISTS "Allow user insert for authenticator" ON "public"."users"; +DROP POLICY IF EXISTS "Allow user insert for dashboard_user" ON "public"."users"; + +-- Create the correct policies +DROP POLICY IF EXISTS "Allow user profile creation during registration" ON "public"."users"; +CREATE POLICY "Allow user profile creation during registration" ON "public"."users" +FOR INSERT TO "anon" WITH CHECK (true); + +DROP POLICY IF EXISTS "Allow authenticated user profile creation" ON "public"."users"; +CREATE POLICY "Allow authenticated user profile creation" ON "public"."users" +FOR INSERT TO "authenticated" WITH CHECK (auth.uid() = id); + +DROP POLICY IF EXISTS "Allow user profile creation for service role" ON "public"."users"; +CREATE POLICY "Allow user profile creation for service role" ON "public"."users" +FOR INSERT TO "service_role" WITH CHECK (true); + +-- Refresh schema cache +NOTIFY pgrst, 'reload schema'; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102536_fix_user_rls_comprehensive.sql b/website-monitoring-frontend/supabase/migrations/20250813102536_fix_user_rls_comprehensive.sql new file mode 100644 index 0000000..f50ac47 --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102536_fix_user_rls_comprehensive.sql @@ -0,0 +1,60 @@ +-- Comprehensive fix for user RLS policies +-- Drop ALL existing policies and recreate them correctly + +-- First, disable RLS temporarily to clear all policies +ALTER TABLE "public"."users" DISABLE ROW LEVEL SECURITY; + +-- Re-enable RLS +ALTER TABLE "public"."users" ENABLE ROW LEVEL SECURITY; + +-- Drop ALL existing policies (if they exist) +DROP POLICY IF EXISTS "Allow user insert for anon" ON "public"."users"; +DROP POLICY IF EXISTS "Allow user insert for authenticated" ON "public"."users"; +DROP POLICY IF EXISTS "Allow user insert for authenticator" ON "public"."users"; +DROP POLICY IF EXISTS "Allow user insert for dashboard_user" ON "public"."users"; +DROP POLICY IF EXISTS "Allow user profile creation during registration" ON "public"."users"; +DROP POLICY IF EXISTS "Allow authenticated user profile creation" ON "public"."users"; +DROP POLICY IF EXISTS "Allow user profile creation for service role" ON "public"."users"; +DROP POLICY IF EXISTS "Users can view their own profile" ON "public"."users"; +DROP POLICY IF EXISTS "Users can update their own profile" ON "public"."users"; +DROP POLICY IF EXISTS "Users can view profiles in their organization" ON "public"."users"; + +-- Create comprehensive policies for all scenarios +-- 1. Allow anonymous users to create profiles during registration +CREATE POLICY "Allow user profile creation during registration" ON "public"."users" +FOR INSERT TO "anon" WITH CHECK (true); + +-- 2. Allow authenticated users to create their own profile +CREATE POLICY "Allow authenticated user profile creation" ON "public"."users" +FOR INSERT TO "authenticated" WITH CHECK (auth.uid() = id); + +-- 3. Allow service role to create user profiles +CREATE POLICY "Allow user profile creation for service role" ON "public"."users" +FOR INSERT TO "service_role" WITH CHECK (true); + +-- 4. Allow users to view their own profile +CREATE POLICY "Users can view their own profile" ON "public"."users" +FOR SELECT TO "authenticated" USING (auth.uid() = id); + +-- 5. Allow users to view profiles in their organization +CREATE POLICY "Users can view profiles in their organization" ON "public"."users" +FOR SELECT TO "authenticated" USING ( + organization_id IN ( + SELECT organization_id FROM "public"."users" WHERE id = auth.uid() + ) +); + +-- 6. Allow users to update their own profile +CREATE POLICY "Users can update their own profile" ON "public"."users" +FOR UPDATE TO "authenticated" USING (auth.uid() = id); + +-- 7. Allow service role to view all users +CREATE POLICY "Service role can view all users" ON "public"."users" +FOR SELECT TO "service_role" USING (true); + +-- 8. Allow service role to update all users +CREATE POLICY "Service role can update all users" ON "public"."users" +FOR UPDATE TO "service_role" USING (true); + +-- Refresh schema cache +NOTIFY pgrst, 'reload schema'; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102537_fix_user_rls_final.sql b/website-monitoring-frontend/supabase/migrations/20250813102537_fix_user_rls_final.sql new file mode 100644 index 0000000..c9ede83 --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102537_fix_user_rls_final.sql @@ -0,0 +1,8 @@ +-- Final fix for user RLS - temporarily disable RLS to allow user profile creation +-- This is a temporary solution to get the registration working + +-- Disable RLS on users table completely +ALTER TABLE "public"."users" DISABLE ROW LEVEL SECURITY; + +-- Refresh schema cache +NOTIFY pgrst, 'reload schema'; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102538_fix_handle_new_user.sql b/website-monitoring-frontend/supabase/migrations/20250813102538_fix_handle_new_user.sql new file mode 100644 index 0000000..e943291 --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102538_fix_handle_new_user.sql @@ -0,0 +1,66 @@ +-- Fix the handle_new_user function to use existing organization if provided +CREATE OR REPLACE FUNCTION public.handle_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public', 'auth' +AS $function$ +DECLARE + organization_id UUID; +BEGIN + -- Check if organization_id is provided in metadata + IF NEW.raw_user_meta_data->>'organization_id' IS NOT NULL THEN + -- Use the existing organization + organization_id := (NEW.raw_user_meta_data->>'organization_id')::UUID; + ELSE + -- Create new organization if none provided + INSERT INTO public.organizations ( + name, + subscription_tier, + subscription_status + ) VALUES ( + COALESCE(NEW.raw_user_meta_data->>'organization_name', NEW.raw_user_meta_data->>'name' || '''s Organization'), + 'free', + 'active' + ) + RETURNING id INTO organization_id; + END IF; + + -- Create the user profile + INSERT INTO public.users ( + id, + email, + name, + organization_id, + role, + is_active, + settings, + created_at, + updated_at + ) VALUES ( + NEW.id, + NEW.email, + COALESCE(NEW.raw_user_meta_data->>'name', split_part(NEW.email, '@', 1)), + organization_id, + COALESCE(NEW.raw_user_meta_data->>'role', 'owner')::user_role, + true, + jsonb_build_object( + 'email_notifications', true, + 'notification_frequency', 'instant', + 'dashboard_layout', 'default' + ), + NOW(), + NOW() + ); + + RETURN NEW; +EXCEPTION + WHEN others THEN + -- Log the error (will appear in Postgres logs) + RAISE LOG 'Error in handle_new_user: %', SQLERRM; + RETURN NEW; +END; +$function$; + +-- Refresh schema cache +NOTIFY pgrst, 'reload schema'; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102539_fix_crawl_sessions.sql b/website-monitoring-frontend/supabase/migrations/20250813102539_fix_crawl_sessions.sql new file mode 100644 index 0000000..3f29581 --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102539_fix_crawl_sessions.sql @@ -0,0 +1,11 @@ +-- Fix crawl_sessions table by adding missing columns +-- Add missing columns to crawl_sessions table +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS processed_urls TEXT[] DEFAULT ARRAY[]::TEXT[]; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS pages_discovered INTEGER DEFAULT 0; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS pages_processed INTEGER DEFAULT 0; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS current_url VARCHAR; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS error_message TEXT; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb; + +-- Refresh schema cache to pick up new columns +NOTIFY pgrst, 'reload schema'; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102540_fix_user_select_rls.sql b/website-monitoring-frontend/supabase/migrations/20250813102540_fix_user_select_rls.sql new file mode 100644 index 0000000..f5caf09 --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102540_fix_user_select_rls.sql @@ -0,0 +1,21 @@ +-- Fix RLS policies for users table to allow proper user details loading +-- Add policies for SELECT operations on users table + +-- Allow users to select their own profile +CREATE POLICY "Users can select their own profile" ON public.users +FOR SELECT USING (auth.uid() = id); + +-- Allow users to select profiles in their organization +CREATE POLICY "Users can select profiles in their organization" ON public.users +FOR SELECT USING ( + organization_id IN ( + SELECT organization_id FROM public.users WHERE id = auth.uid() + ) +); + +-- Allow service role to select any user (for admin operations) +CREATE POLICY "Service role can select any user" ON public.users +FOR SELECT USING (auth.role() = 'service_role'); + +-- Refresh schema cache +NOTIFY pgrst, 'reload schema'; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102541_fix_crawl_sessions_progress.sql b/website-monitoring-frontend/supabase/migrations/20250813102541_fix_crawl_sessions_progress.sql new file mode 100644 index 0000000..420411f --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102541_fix_crawl_sessions_progress.sql @@ -0,0 +1,6 @@ +-- Fix crawl_sessions table by adding missing progress_percentage column +-- Add missing progress_percentage column to crawl_sessions table +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS progress_percentage INTEGER DEFAULT 0; + +-- Refresh schema cache to pick up new column +NOTIFY pgrst, 'reload schema'; diff --git a/website-monitoring-frontend/supabase/migrations/20250813102542_add_missing_crawl_session_columns.sql b/website-monitoring-frontend/supabase/migrations/20250813102542_add_missing_crawl_session_columns.sql new file mode 100644 index 0000000..27e5e50 --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102542_add_missing_crawl_session_columns.sql @@ -0,0 +1,8 @@ +-- Add missing columns to crawl_sessions table that the API expects +-- Add total_urls and processed_urls columns that the API is trying to insert +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS total_urls INTEGER DEFAULT 0; +ALTER TABLE crawl_sessions ADD COLUMN IF NOT EXISTS processed_urls INTEGER DEFAULT 0; + +-- Refresh schema cache to pick up new columns +NOTIFY pgrst, 'reload schema'; + diff --git a/website-monitoring-frontend/supabase/migrations/20250813102546_enable_realtime_crawl_sessions.sql b/website-monitoring-frontend/supabase/migrations/20250813102546_enable_realtime_crawl_sessions.sql new file mode 100644 index 0000000..a4c27b0 --- /dev/null +++ b/website-monitoring-frontend/supabase/migrations/20250813102546_enable_realtime_crawl_sessions.sql @@ -0,0 +1,6 @@ +-- Enable realtime for crawl_sessions table +ALTER PUBLICATION supabase_realtime ADD TABLE crawl_sessions; + +-- Also enable for pages table for real-time page updates +ALTER PUBLICATION supabase_realtime ADD TABLE pages; + diff --git a/website-monitoring-frontend/tsconfig.json b/website-monitoring-frontend/tsconfig.json new file mode 100644 index 0000000..e59724b --- /dev/null +++ b/website-monitoring-frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/website-monitoring-frontend/vercel.json b/website-monitoring-frontend/vercel.json new file mode 100644 index 0000000..9b15976 --- /dev/null +++ b/website-monitoring-frontend/vercel.json @@ -0,0 +1,13 @@ +{ + "crons": [ + { + "path": "/api/cron/scan?mode=all", + "schedule": "0 */6 * * *" + } + ], + "functions": { + "app/api/cron/scan/route.ts": { + "maxDuration": 300 + } + } +} \ No newline at end of file